diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 80dd5af12afb..0f0cb0adf66f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -28,7 +28,6 @@ import android.print.PrintAttributes import android.view.MenuItem import android.view.MotionEvent import android.view.View -import android.webkit.GeolocationPermissions import android.webkit.HttpAuthHandler import android.webkit.PermissionRequest import android.webkit.SslErrorHandler @@ -141,10 +140,7 @@ import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactoryImpl import com.duckduckgo.app.location.GeoLocationPermissions -import com.duckduckgo.app.location.data.LocationPermissionEntity -import com.duckduckgo.app.location.data.LocationPermissionType import com.duckduckgo.app.location.data.LocationPermissionsDao -import com.duckduckgo.app.location.data.LocationPermissionsRepositoryImpl import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.AppStage.ESTABLISHED import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -242,6 +238,7 @@ import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.site.permissions.api.SitePermissionsManager +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissionQueryResponse import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions import com.duckduckgo.subscriptions.api.Subscriptions @@ -549,6 +546,7 @@ class BrowserTabViewModelTest { whenever(changeOmnibarPositionFeature.refactor()).thenReturn(mockEnabledToggle) whenever(mockAutocompleteTabsFeature.self()).thenReturn(mockEnabledToggle) whenever(mockAutocompleteTabsFeature.self().isEnabled()).thenReturn(true) + whenever(mockSitePermissionsManager.hasSitePermanentPermission(any(), any())).thenReturn(false) remoteMessagingModel = givenRemoteMessagingModel(mockRemoteMessagingRepository, mockPixel, coroutineRule.testDispatcherProvider) @@ -624,12 +622,6 @@ class BrowserTabViewModelTest { dispatchers = coroutineRule.testDispatcherProvider, fireproofWebsiteRepository = fireproofWebsiteRepositoryImpl, savedSitesRepository = mockSavedSitesRepository, - locationPermissionsRepository = LocationPermissionsRepositoryImpl( - locationPermissionsDao, - lazyFaviconManager, - coroutineRule.testDispatcherProvider, - ), - geoLocationPermissions = geoLocationPermissions, navigationAwareLoginDetector = mockNavigationAwareLoginDetector, userEventsStore = mockUserEventsStore, fileDownloader = mockFileDownloader, @@ -2988,261 +2980,14 @@ class BrowserTabViewModelTest { assertCommandIssued() } - @Test - fun whenDeviceLocationSharingIsDisabledThenSitePermissionIsDenied() = runTest { - val domain = "https://www.example.com/" - - givenDeviceLocationSharingIsEnabled(false) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenCurrentDomainAndPermissionRequestingDomainAreDifferentThenSitePermissionIsDenied() = runTest { - givenDeviceLocationSharingIsEnabled(true) - givenCurrentSite("https://wwww.example.com/") - givenNewPermissionRequestFromDomain("https://wwww.anotherexample.com/") - - verify(geoLocationPermissions).clear("https://wwww.anotherexample.com/") - } - - @Test - fun whenDomainRequestsSitePermissionThenAppChecksSystemLocationPermission() = runTest { - val domain = "https://www.example.com/" - - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - assertCommandIssued() - } - - @Test - fun whenDomainRequestsSitePermissionAndAlreadyRepliedThenAppChecksSystemLocationPermission() = runTest { - val domain = "https://www.example.com/" - - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.DENY_ALWAYS) - - givenNewPermissionRequestFromDomain(domain) - - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenDomainRequestsSitePermissionAndAllowedThenAppChecksSystemLocationPermission() = runTest { - val domain = "https://www.example.com/" - - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.ALLOW_ALWAYS) - - givenNewPermissionRequestFromDomain(domain) - - assertCommandIssued() - } - - @Test - fun whenDomainRequestsSitePermissionAndUserAllowedSessionPermissionThenPermissionIsAllowed() = runTest { - val domain = "https://www.example.com/" - - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - testee.onSiteLocationPermissionSelected(domain, LocationPermissionType.ALLOW_ONCE) - - givenNewPermissionRequestFromDomain(domain) - - assertCommandIssuedTimes(times = 1) - } - - @Test - fun whenAppLocationPermissionIsDeniedThenSitePermissionIsDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(false) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenSystemPermissionIsDeniedThenSitePermissionIsCleared() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionDeniedOneTime() - - verify(mockPixel).fire(AppPixelName.PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_DISABLE) - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenUserGrantsSystemLocationPermissionThenSettingsLocationPermissionShouldBeEnabled() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - verify(mockSettingsStore).appLocationPermission = true - } - - @Test - fun whenUserGrantsSystemLocationPermissionThenPixelIsFired() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - verify(mockPixel).fire(AppPixelName.PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_ENABLE) - } - - @Test - fun whenUserChoosesToAlwaysAllowSitePermissionThenGeoPermissionIsAllowed() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.ALLOW_ALWAYS) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - verify(geoLocationPermissions, atLeastOnce()).allow(domain) - } - - @Test - fun whenUserChoosesToAlwaysDenySitePermissionThenGeoPermissionIsAllowed() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.DENY_ALWAYS) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - verify(geoLocationPermissions, atLeastOnce()).clear(domain) - } - - @Test - fun whenUserChoosesToAllowSitePermissionThenGeoPermissionIsAllowed() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.ALLOW_ONCE) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - assertCommandIssued() - } - - @Test - fun whenUserChoosesToDenySitePermissionThenGeoPermissionIsAllowed() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.DENY_ONCE) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - assertCommandIssued() - } - - @Test - fun whenNewDomainRequestsForPermissionThenUserShouldBeAskedToGivePermission() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - assertCommandIssued() - } - - @Test - fun whenSystemLocationPermissionIsDeniedThenSitePermissionIsDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionNotAllowed() - - verify(mockPixel).fire(AppPixelName.PRECISE_LOCATION_SYSTEM_DIALOG_LATER) - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenSystemLocationPermissionIsNeverAllowedThenSitePermissionIsDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionNeverAllowed() - - verify(mockPixel).fire(AppPixelName.PRECISE_LOCATION_SYSTEM_DIALOG_NEVER) - verify(geoLocationPermissions).clear(domain) - assertEquals(locationPermissionsDao.getPermission(domain)!!.permission, LocationPermissionType.DENY_ALWAYS) - } - - @Test - fun whenSystemLocationPermissionIsAllowedThenAppAsksForSystemPermission() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionAllowed() - - assertCommandIssued() - } - - @Test - fun whenUserDeniesSitePermissionThenSitePermissionIsDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSiteLocationPermissionAlwaysDenied() - - verify(geoLocationPermissions).clear(domain) - } - @Test fun whenUserVisitsDomainWithPermanentLocationPermissionThenMessageIsShown() = runTest { val domain = "https://www.example.com/" - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.ALLOW_ALWAYS) + whenever(mockSitePermissionsManager.hasSitePermanentPermission(domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION)).thenReturn( + true, + ) + givenCurrentSite(domain) givenDeviceLocationSharingIsEnabled(true) givenLocationPermissionIsEnabled(true) @@ -3256,7 +3001,10 @@ class BrowserTabViewModelTest { fun whenUserVisitsDomainWithoutPermanentLocationPermissionThenMessageIsNotShown() = runTest { val domain = "https://www.example.com/" - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.DENY_ALWAYS) + whenever(mockSitePermissionsManager.hasSitePermanentPermission(domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION)).thenReturn( + false, + ) + givenCurrentSite(domain) givenDeviceLocationSharingIsEnabled(true) givenLocationPermissionIsEnabled(true) @@ -3293,7 +3041,10 @@ class BrowserTabViewModelTest { fun whenUserRefreshesASiteLocationMessageIsNotShownAgain() = runTest { val domain = "https://www.example.com/" - givenUserAlreadySelectedPermissionForDomain(domain, LocationPermissionType.ALLOW_ALWAYS) + whenever(mockSitePermissionsManager.hasSitePermanentPermission(domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION)).thenReturn( + true, + ) + givenCurrentSite(domain) givenDeviceLocationSharingIsEnabled(true) givenLocationPermissionIsEnabled(true) @@ -3303,73 +3054,6 @@ class BrowserTabViewModelTest { assertCommandIssuedTimes(1) } - @Test - fun whenUserSelectsPermissionAndRefreshesPageThenLocationMessageIsNotShown() = runTest { - val domain = "http://example.com" - - givenCurrentSite(domain) - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - - testee.onSiteLocationPermissionSelected(domain, LocationPermissionType.ALLOW_ALWAYS) - - loadUrl(domain, isBrowserShowing = true) - - assertCommandNotIssued() - } - - @Test - fun whenSystemLocationPermissionIsDeniedThenSiteLocationPermissionIsAlwaysDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionDeniedOneTime() - - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenSystemLocationPermissionIsDeniedForeverThenSiteLocationPermissionIsAlwaysDenied() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionDeniedForever() - - verify(geoLocationPermissions).clear(domain) - } - - @Test - fun whenSystemLocationPermissionIsDeniedForeverThenSettingsFlagIsUpdated() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionDeniedForever() - - verify(mockSettingsStore).appLocationPermissionDeniedForever = true - } - - @Test - fun whenSystemLocationIsGrantedThenSettingsFlagIsUpdated() = runTest { - val domain = "https://www.example.com/" - givenDeviceLocationSharingIsEnabled(true) - givenLocationPermissionIsEnabled(true) - givenCurrentSite(domain) - givenNewPermissionRequestFromDomain(domain) - - testee.onSystemLocationPermissionGranted() - - verify(mockSettingsStore).appLocationPermissionDeniedForever = false - } - @Test fun whenPrefetchFaviconThenFetchFaviconForCurrentTab() = runTest { val url = "https://www.example.com/" @@ -3486,38 +3170,6 @@ class BrowserTabViewModelTest { verify(mockFaviconManager, never()).storeFavicon(any(), any()) } - @Test - fun whenOnSiteLocationPermissionSelectedAndPermissionIsAllowAlwaysThenPersistFavicon() = runTest { - val url = "http://example.com" - val permission = LocationPermissionType.ALLOW_ALWAYS - givenNewPermissionRequestFromDomain(url) - - testee.onSiteLocationPermissionSelected(url, permission) - - verify(mockFaviconManager).persistCachedFavicon(any(), eq(url)) - } - - @Test - fun whenOnSiteLocationPermissionSelectedAndPermissionIsDenyAlwaysThenPersistFavicon() = runTest { - val url = "http://example.com" - val permission = LocationPermissionType.DENY_ALWAYS - givenNewPermissionRequestFromDomain(url) - - testee.onSiteLocationPermissionSelected(url, permission) - - verify(mockFaviconManager).persistCachedFavicon(any(), eq(url)) - } - - @Test - fun whenOnSystemLocationPermissionNeverAllowedThenPersistFavicon() = runTest { - val url = "http://example.com" - givenNewPermissionRequestFromDomain(url) - - testee.onSystemLocationPermissionNeverAllowed() - - verify(mockFaviconManager).persistCachedFavicon(any(), eq(url)) - } - @Test fun whenBookmarkAddedThenPersistFavicon() = runTest { val url = "http://example.com" @@ -6181,10 +5833,6 @@ class BrowserTabViewModelTest { dismissedCtaDaoChannel.send(listOf(DismissedCta(CtaId.DAX_DIALOG_TRACKERS_FOUND))) } - private fun givenNewPermissionRequestFromDomain(domain: String) { - testee.onSiteLocationPermissionRequested(domain, StubPermissionCallback()) - } - private fun givenDeviceLocationSharingIsEnabled(state: Boolean) { whenever(geoLocationPermissions.isDeviceLocationEnabled()).thenReturn(state) } @@ -6193,23 +5841,6 @@ class BrowserTabViewModelTest { whenever(mockSettingsStore.appLocationPermission).thenReturn(state) } - private fun givenUserAlreadySelectedPermissionForDomain( - domain: String, - permission: LocationPermissionType, - ) { - locationPermissionsDao.insert(LocationPermissionEntity(domain, permission)) - } - - class StubPermissionCallback : GeolocationPermissions.Callback { - override fun invoke( - p0: String?, - p1: Boolean, - p2: Boolean, - ) { - // nothing to see - } - } - private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) val issuedCommand = commandCaptor.allValues.find { it is T } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt index 366645e15323..f6755db8698f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserChromeClient.kt @@ -35,6 +35,7 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DefaultDispatcherProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.site.permissions.api.SitePermissionsManager +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -144,16 +145,26 @@ class BrowserChromeClient @Inject constructor( } override fun onPermissionRequest(request: PermissionRequest) { + Timber.d("Permissions: permission requested ${request.resources.asList()}") webViewClientListener?.getCurrentTabId()?.let { tabId -> appCoroutineScope.launch(coroutineDispatcher.io()) { val permissionsAllowedToAsk = sitePermissionsManager.getSitePermissions(tabId, request) if (permissionsAllowedToAsk.userHandled.isNotEmpty()) { + Timber.d("Permissions: permission requested not user handled") webViewClientListener?.onSitePermissionRequested(request, permissionsAllowedToAsk) } } } } + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback, + ) { + Timber.d("Permissions: location permission requested $origin") + onPermissionRequest(LocationPermissionRequest(origin, callback)) + } + override fun onCloseWindow(window: WebView?) { webViewClientListener?.closeCurrentTab() } @@ -208,13 +219,6 @@ class BrowserChromeClient @Inject constructor( return true } - override fun onGeolocationPermissionsShowPrompt( - origin: String, - callback: GeolocationPermissions.Callback, - ) { - webViewClientListener?.onSiteLocationPermissionRequested(origin, callback) - } - override fun getDefaultVideoPoster(): Bitmap { return Bitmap.createBitmap(intArrayOf(Color.TRANSPARENT), 1, 1, ARGB_8888) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 92835967bbf3..73ba576b7c78 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -70,7 +70,6 @@ import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.AnyThread import androidx.annotation.StringRes -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.core.text.HtmlCompat @@ -100,7 +99,6 @@ import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.brokensite.BrokenSiteActivity import com.duckduckgo.app.browser.BrowserTabViewModel.FileChooserRequestedParams -import com.duckduckgo.app.browser.BrowserTabViewModel.LocationPermission import com.duckduckgo.app.browser.R.string import com.duckduckgo.app.browser.SSLErrorType.NONE import com.duckduckgo.app.browser.WebViewErrorResponse.LOADING @@ -118,8 +116,6 @@ import com.duckduckgo.app.browser.cookies.ThirdPartyCookieManager import com.duckduckgo.app.browser.customtabs.CustomTabActivity import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames import com.duckduckgo.app.browser.customtabs.CustomTabViewModel.Companion.CUSTOM_TAB_NAME_PREFIX -import com.duckduckgo.app.browser.databinding.ContentSiteLocationPermissionDialogBinding -import com.duckduckgo.app.browser.databinding.ContentSystemLocationPermissionDialogBinding import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding import com.duckduckgo.app.browser.databinding.HttpAuthenticationBinding import com.duckduckgo.app.browser.downloader.BlobConverterInjector @@ -186,7 +182,6 @@ import com.duckduckgo.app.global.view.isImmersiveModeEnabled import com.duckduckgo.app.global.view.launchDefaultAppActivity import com.duckduckgo.app.global.view.renderIfChanged import com.duckduckgo.app.global.view.toggleFullScreen -import com.duckduckgo.app.location.data.LocationPermissionType import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.privatesearch.PrivateSearchScreenNoParams import com.duckduckgo.app.settings.db.SettingsDataStore @@ -297,7 +292,6 @@ import com.duckduckgo.voice.api.VoiceSearchLauncher import com.duckduckgo.voice.api.VoiceSearchLauncher.Source.BROWSER import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import java.io.File @@ -1661,9 +1655,6 @@ class BrowserTabFragment : is Command.ShowErrorWithAction -> showErrorSnackbar(it) is Command.HideWebContent -> webView?.hide() is Command.ShowWebContent -> webView?.show() - is Command.CheckSystemLocationPermission -> checkSystemLocationPermission(it.domain, it.deniedForever) - is Command.RequestSystemLocationPermission -> requestLocationPermissions() - is Command.AskDomainPermission -> askSiteLocationPermission(it.locationPermission) is Command.RefreshUserAgent -> refreshUserAgent(it.url, it.isDesktop) is Command.AskToFireproofWebsite -> askToFireproofWebsite(requireContext(), it.fireproofWebsite) is Command.AskToAutomateFireproofWebsite -> askToAutomateFireproofWebsite(requireContext(), it.fireproofWebsite) @@ -1909,116 +1900,6 @@ class BrowserTabFragment : } } - private fun locationPermissionsHaveNotBeenGranted(): Boolean { - return ContextCompat.checkSelfPermission( - requireActivity(), - Manifest.permission.ACCESS_COARSE_LOCATION, - ) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED - } - - private fun checkSystemLocationPermission( - domain: String, - deniedForever: Boolean, - ) { - if (locationPermissionsHaveNotBeenGranted()) { - if (deniedForever) { - viewModel.onSystemLocationPermissionDeniedForever() - } else { - showSystemLocationPermissionDialog(domain) - } - } else { - viewModel.onSystemLocationPermissionGranted() - } - } - - private fun showSystemLocationPermissionDialog(domain: String) { - val binding = ContentSystemLocationPermissionDialogBinding.inflate(layoutInflater) - - val originUrl = domain.websiteFromGeoLocationsApiOrigin() - val subtitle = getString(R.string.preciseLocationSystemDialogSubtitle, originUrl, originUrl) - binding.systemPermissionDialogSubtitle.text = subtitle - - val dialog = CustomAlertDialogBuilder(requireActivity()) - .setView(binding) - .build() - - binding.allowLocationPermission.setOnClickListener { - viewModel.onSystemLocationPermissionAllowed() - dialog.dismiss() - } - - binding.denyLocationPermission.setOnClickListener { - viewModel.onSystemLocationPermissionNotAllowed() - dialog.dismiss() - } - - binding.neverAllowLocationPermission.setOnClickListener { - viewModel.onSystemLocationPermissionNeverAllowed() - dialog.dismiss() - } - - dialog.show() - } - - private fun requestLocationPermissions() { - requestPermissions( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - PERMISSION_REQUEST_GEO_LOCATION, - ) - } - - private fun askSiteLocationPermission(locationPermission: LocationPermission) { - if (!isActiveCustomTab() && !isActiveTab) { - Timber.v("Will not launch a dialog for an inactive tab") - return - } - - val binding = ContentSiteLocationPermissionDialogBinding.inflate(layoutInflater) - - val domain = locationPermission.origin - val title = domain.websiteFromGeoLocationsApiOrigin() - binding.sitePermissionDialogTitle.text = getString(R.string.preciseLocationSiteDialogTitle, title) - binding.sitePermissionDialogSubtitle.text = if (title == DDG_DOMAIN) { - getString(R.string.preciseLocationDDGDialogSubtitle) - } else { - getString(R.string.preciseLocationSiteDialogSubtitle) - } - - val dialog = MaterialAlertDialogBuilder(requireActivity()) - .setView(binding.root) - .setOnCancelListener { - // Called when user clicks outside the dialog - deny to be safe - locationPermission.callback.invoke(locationPermission.origin, false, false) - } - .create() - - binding.siteAllowAlwaysLocationPermission.setOnClickListener { - viewModel.onSiteLocationPermissionSelected(domain, LocationPermissionType.ALLOW_ALWAYS) - dialog.dismiss() - } - - binding.siteAllowOnceLocationPermission.setOnClickListener { - viewModel.onSiteLocationPermissionSelected(domain, LocationPermissionType.ALLOW_ONCE) - dialog.dismiss() - } - - binding.siteDenyOnceLocationPermission.setOnClickListener { - viewModel.onSiteLocationPermissionSelected(domain, LocationPermissionType.DENY_ONCE) - dialog.dismiss() - } - - binding.siteDenyAlwaysLocationPermission.setOnClickListener { - viewModel.onSiteLocationPermissionSelected(domain, LocationPermissionType.DENY_ALWAYS) - dialog.dismiss() - } - - dialog.show() - } - private fun launchBrokenSiteFeedback(data: BrokenSiteData) { val context = context ?: return @@ -3522,18 +3403,6 @@ class BrowserTabFragment : omnibar.toolbar.makeSnackbarWithNoBottomInset(R.string.permissionRequiredToDownload, Snackbar.LENGTH_LONG).show() } } - - PERMISSION_REQUEST_GEO_LOCATION -> { - if ((grantResults.isNotEmpty()) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - viewModel.onSystemLocationPermissionGranted() - } else { - if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION)) { - viewModel.onSystemLocationPermissionDeniedOneTime() - } else { - viewModel.onSystemLocationPermissionDeniedTwice() - } - } - } } } @@ -3632,7 +3501,6 @@ class BrowserTabFragment : private const val TAB_ID_ARG = "TAB_ID_ARG" private const val URL_EXTRA_ARG = "URL_EXTRA_ARG" private const val SKIP_HOME_ARG = "SKIP_HOME_ARG" - private const val DDG_DOMAIN = "duckduckgo.com" private const val LAUNCH_FROM_EXTERNAL_EXTRA = "LAUNCH_FROM_EXTERNAL_EXTRA" const val ADD_SAVED_SITE_FRAGMENT_TAG = "ADD_SAVED_SITE" @@ -3642,7 +3510,6 @@ class BrowserTabFragment : private const val REQUEST_CODE_CHOOSE_FILE = 100 private const val PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 200 - private const val PERMISSION_REQUEST_GEO_LOCATION = 300 private const val URL_BUNDLE_KEY = "url" diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index a86557bdedb4..abab324aaaa2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -28,7 +28,6 @@ import android.view.ContextMenu import android.view.MenuItem import android.view.MotionEvent.ACTION_UP import android.view.View -import android.webkit.GeolocationPermissions import android.webkit.MimeTypeMap import android.webkit.PermissionRequest import android.webkit.SslErrorHandler @@ -77,14 +76,12 @@ import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository import com.duckduckgo.app.browser.certificates.remoteconfig.SSLCertificatesFeature import com.duckduckgo.app.browser.commands.Command import com.duckduckgo.app.browser.commands.Command.AddHomeShortcut -import com.duckduckgo.app.browser.commands.Command.AskDomainPermission import com.duckduckgo.app.browser.commands.Command.AskToAutomateFireproofWebsite import com.duckduckgo.app.browser.commands.Command.AskToDisableLoginDetection import com.duckduckgo.app.browser.commands.Command.AskToFireproofWebsite import com.duckduckgo.app.browser.commands.Command.AutocompleteItemRemoved import com.duckduckgo.app.browser.commands.Command.BrokenSiteFeedback import com.duckduckgo.app.browser.commands.Command.CancelIncomingAutofillRequest -import com.duckduckgo.app.browser.commands.Command.CheckSystemLocationPermission import com.duckduckgo.app.browser.commands.Command.ChildTabClosed import com.duckduckgo.app.browser.commands.Command.ConvertBlobToDataUri import com.duckduckgo.app.browser.commands.Command.CopyAliasToClipboard @@ -120,7 +117,6 @@ import com.duckduckgo.app.browser.commands.Command.OpenMessageInNewTab import com.duckduckgo.app.browser.commands.Command.PrintLink import com.duckduckgo.app.browser.commands.Command.RefreshUserAgent import com.duckduckgo.app.browser.commands.Command.RequestFileDownload -import com.duckduckgo.app.browser.commands.Command.RequestSystemLocationPermission import com.duckduckgo.app.browser.commands.Command.RequiresAuthentication import com.duckduckgo.app.browser.commands.Command.ResetHistory import com.duckduckgo.app.browser.commands.Command.SaveCredentials @@ -220,9 +216,7 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl -import com.duckduckgo.app.location.GeoLocationPermissions import com.duckduckgo.app.location.data.LocationPermissionType -import com.duckduckgo.app.location.data.LocationPermissionsRepository import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_BANNER_DISMISSED @@ -301,6 +295,7 @@ import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.DeleteBookmarkListener import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.EditSavedSiteListener import com.duckduckgo.site.permissions.api.SitePermissionsManager +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissionQueryResponse import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions import com.duckduckgo.subscriptions.api.Subscriptions @@ -378,8 +373,6 @@ class BrowserTabViewModel @Inject constructor( private val networkLeaderboardDao: NetworkLeaderboardDao, private val savedSitesRepository: SavedSitesRepository, private val fireproofWebsiteRepository: FireproofWebsiteRepository, - private val locationPermissionsRepository: LocationPermissionsRepository, - private val geoLocationPermissions: GeoLocationPermissions, private val navigationAwareLoginDetector: NavigationAwareLoginDetector, private val autoComplete: AutoComplete, private val appSettingsPreferencesStore: SettingsDataStore, @@ -446,11 +439,6 @@ class BrowserTabViewModel @Inject constructor( // Map>() = Map>() private val fixedReplyProxyMap = mutableMapOf>() - data class LocationPermission( - val origin: String, - val callback: GeolocationPermissions.Callback, - ) - data class FileChooserRequestedParams( val filePickingMode: Int, val acceptMimeTypes: List, @@ -496,7 +484,7 @@ class BrowserTabViewModel @Inject constructor( val title: String? get() = site?.title - private var locationPermission: LocationPermission? = null + private var locationPermissionRequest: LocationPermissionRequest? = null private val locationPermissionMessages: MutableMap = mutableMapOf() private val locationPermissionSession: MutableMap = mutableMapOf() @@ -1605,41 +1593,12 @@ class BrowserTabViewModel @Inject constructor( } private suspend fun notifyPermanentLocationPermission(domain: String) { - if (!geoLocationPermissions.isDeviceLocationEnabled()) { - viewModelScope.launch(dispatchers.io()) { - onDeviceLocationDisabled() - } - return - } - - if (!appSettingsPreferencesStore.appLocationPermission) { - return - } - - val permissionEntity = locationPermissionsRepository.getDomainPermission(domain) - permissionEntity?.let { - if (it.permission == LocationPermissionType.ALLOW_ALWAYS) { - Timber.d("Location Permission: domain $domain site url ${site?.url}") - if (!locationPermissionMessages.containsKey(domain)) { - setDomainHasLocationPermissionShown(domain) - if (shouldShowLocationPermissionMessage()) { - Timber.d("Show location permission for $domain") - command.postValue(ShowDomainHasPermissionMessage(domain)) - } - } - } + if (sitePermissionsManager.hasSitePermanentPermission(domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION)) { + Timber.d("Location Permission: domain $domain site url ${site?.url} has location permission") + command.postValue(ShowDomainHasPermissionMessage(domain)) } } - private fun shouldShowLocationPermissionMessage(): Boolean { - val url = site?.url ?: return true - return !duckDuckGoUrlDetector.isDuckDuckGoChatUrl(url) - } - - private fun setDomainHasLocationPermissionShown(domain: String) { - locationPermissionMessages[domain] = true - } - private fun urlUpdated(url: String) { Timber.v("Page url updated: $url") site?.url = url @@ -1744,51 +1703,16 @@ class BrowserTabViewModel @Inject constructor( request: PermissionRequest, sitePermissionsAllowedToAsk: SitePermissions, ) { - viewModelScope.launch(dispatchers.io()) { - command.postValue(ShowSitePermissionsDialog(sitePermissionsAllowedToAsk, request)) - } - } - - override fun onSiteLocationPermissionRequested( - origin: String, - callback: GeolocationPermissions.Callback, - ) { - locationPermission = LocationPermission(origin, callback) - - if (!geoLocationPermissions.isDeviceLocationEnabled()) { - viewModelScope.launch(dispatchers.io()) { - onDeviceLocationDisabled() + if (request is LocationPermissionRequest) { + if (!sameEffectiveTldPlusOne(site, request.origin)) { + Timber.d("Permissions: sameEffectiveTldPlusOne false") + request.deny() + return } - onSiteLocationPermissionAlwaysDenied() - return } - if (!sameEffectiveTldPlusOne(site, origin)) { - onSiteLocationPermissionAlwaysDenied() - return - } - - if (!appSettingsPreferencesStore.appLocationPermission) { - onSiteLocationPermissionAlwaysDenied() - return - } - - viewModelScope.launch { - val previouslyDeniedForever = appSettingsPreferencesStore.appLocationPermissionDeniedForever - val permissionEntity = locationPermissionsRepository.getDomainPermission(origin) - if (permissionEntity == null) { - if (locationPermissionSession.containsKey(origin)) { - reactToSiteSessionPermission(locationPermissionSession[origin]!!) - } else { - command.postValue(CheckSystemLocationPermission(origin, previouslyDeniedForever)) - } - } else { - if (permissionEntity.permission == LocationPermissionType.DENY_ALWAYS) { - onSiteLocationPermissionAlwaysDenied() - } else { - command.postValue(CheckSystemLocationPermission(origin, previouslyDeniedForever)) - } - } + viewModelScope.launch(dispatchers.io()) { + command.postValue(ShowSitePermissionsDialog(sitePermissionsAllowedToAsk, request)) } } @@ -1805,144 +1729,6 @@ class BrowserTabViewModel @Inject constructor( return siteETldPlusOne == originETldPlusOne } - fun onSiteLocationPermissionSelected( - domain: String, - permission: LocationPermissionType, - ) { - locationPermission?.let { locationPermission -> - when (permission) { - LocationPermissionType.ALLOW_ALWAYS -> { - onSiteLocationPermissionAlwaysAllowed() - setDomainHasLocationPermissionShown(domain) - pixel.fire(AppPixelName.PRECISE_LOCATION_SITE_DIALOG_ALLOW_ALWAYS) - viewModelScope.launch { - locationPermissionsRepository.savePermission(domain, permission) - faviconManager.persistCachedFavicon(tabId, domain) - } - } - - LocationPermissionType.ALLOW_ONCE -> { - pixel.fire(AppPixelName.PRECISE_LOCATION_SITE_DIALOG_ALLOW_ONCE) - locationPermissionSession[domain] = permission - locationPermission.callback.invoke(locationPermission.origin, true, false) - } - - LocationPermissionType.DENY_ALWAYS -> { - pixel.fire(AppPixelName.PRECISE_LOCATION_SITE_DIALOG_DENY_ALWAYS) - onSiteLocationPermissionAlwaysDenied() - viewModelScope.launch { - locationPermissionsRepository.savePermission(domain, permission) - faviconManager.persistCachedFavicon(tabId, domain) - } - } - - LocationPermissionType.DENY_ONCE -> { - pixel.fire(AppPixelName.PRECISE_LOCATION_SITE_DIALOG_DENY_ONCE) - locationPermissionSession[domain] = permission - locationPermission.callback.invoke(locationPermission.origin, false, false) - } - } - } - } - - private fun onSiteLocationPermissionAlwaysAllowed() { - locationPermission?.let { locationPermission -> - geoLocationPermissions.allow(locationPermission.origin) - locationPermission.callback.invoke(locationPermission.origin, true, false) - } - } - - fun onSiteLocationPermissionAlwaysDenied() { - locationPermission?.let { locationPermission -> - geoLocationPermissions.clear(locationPermission.origin) - locationPermission.callback.invoke(locationPermission.origin, false, false) - } - } - - private suspend fun onDeviceLocationDisabled() { - geoLocationPermissions.clearAll() - } - - private fun reactToSitePermission(permission: LocationPermissionType) { - locationPermission?.let { locationPermission -> - when (permission) { - LocationPermissionType.ALLOW_ALWAYS -> { - onSiteLocationPermissionAlwaysAllowed() - } - - LocationPermissionType.ALLOW_ONCE -> { - command.postValue(AskDomainPermission(locationPermission)) - } - - LocationPermissionType.DENY_ALWAYS -> { - onSiteLocationPermissionAlwaysDenied() - } - - LocationPermissionType.DENY_ONCE -> { - command.postValue(AskDomainPermission(locationPermission)) - } - } - } - } - - private fun reactToSiteSessionPermission(permission: LocationPermissionType) { - locationPermission?.let { locationPermission -> - if (permission == LocationPermissionType.ALLOW_ONCE) { - locationPermission.callback.invoke(locationPermission.origin, true, false) - } else { - locationPermission.callback.invoke(locationPermission.origin, false, false) - } - } - } - - fun onSystemLocationPermissionAllowed() { - pixel.fire(AppPixelName.PRECISE_LOCATION_SYSTEM_DIALOG_ENABLE) - command.postValue(RequestSystemLocationPermission) - } - - fun onSystemLocationPermissionNotAllowed() { - pixel.fire(AppPixelName.PRECISE_LOCATION_SYSTEM_DIALOG_LATER) - onSiteLocationPermissionAlwaysDenied() - } - - fun onSystemLocationPermissionNeverAllowed() { - locationPermission?.let { locationPermission -> - onSiteLocationPermissionSelected(locationPermission.origin, LocationPermissionType.DENY_ALWAYS) - pixel.fire(AppPixelName.PRECISE_LOCATION_SYSTEM_DIALOG_NEVER) - } - } - - fun onSystemLocationPermissionGranted() { - locationPermission?.let { locationPermission -> - appSettingsPreferencesStore.appLocationPermissionDeniedForever = false - appSettingsPreferencesStore.appLocationPermission = true - pixel.fire(AppPixelName.PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_ENABLE) - viewModelScope.launch { - val permissionEntity = locationPermissionsRepository.getDomainPermission(locationPermission.origin) - if (permissionEntity == null) { - command.postValue(AskDomainPermission(locationPermission)) - } else { - reactToSitePermission(permissionEntity.permission) - } - } - } - } - - fun onSystemLocationPermissionDeniedOneTime() { - pixel.fire(AppPixelName.PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_DISABLE) - onSiteLocationPermissionAlwaysDenied() - } - - fun onSystemLocationPermissionDeniedTwice() { - pixel.fire(AppPixelName.PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_DISABLE) - onSystemLocationPermissionDeniedForever() - } - - fun onSystemLocationPermissionDeniedForever() { - appSettingsPreferencesStore.appLocationPermissionDeniedForever = true - onSiteLocationPermissionAlwaysDenied() - } - private fun registerSiteVisit() { Schedulers.io().scheduleDirect { networkLeaderboardDao.incrementSitesVisited() diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt index 90df126cfc3c..f8fb1313a391 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -21,7 +21,6 @@ import android.net.Uri import android.net.http.SslCertificate import android.os.Message import android.view.View -import android.webkit.GeolocationPermissions import android.webkit.PermissionRequest import android.webkit.SslErrorHandler import android.webkit.ValueCallback @@ -46,11 +45,6 @@ interface WebViewClientListener { sitePermissionsAllowedToAsk: SitePermissions, ) - fun onSiteLocationPermissionRequested( - origin: String, - callback: GeolocationPermissions.Callback, - ) - fun titleReceived(newTitle: String) fun trackerDetected(event: TrackingEvent) fun pageHasHttpResources(page: String) diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 76f183008781..6453bec1e34a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -27,7 +27,6 @@ import android.webkit.ValueCallback import androidx.annotation.DrawableRes import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.browser.BrowserTabViewModel.FileChooserRequestedParams -import com.duckduckgo.app.browser.BrowserTabViewModel.LocationPermission import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.AppLink import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.NonHttpAppLink import com.duckduckgo.app.browser.SslErrorResponse @@ -160,13 +159,6 @@ sealed class Command { val url: String?, val showDuckPlayerIcon: Boolean = false, ) : Command() - class CheckSystemLocationPermission( - val domain: String, - val deniedForever: Boolean, - ) : Command() - - class AskDomainPermission(val locationPermission: LocationPermission) : Command() - object RequestSystemLocationPermission : Command() class RefreshUserAgent( val url: String?, val isDesktop: Boolean, diff --git a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt index 2535a8ab80db..539a59ef122a 100644 --- a/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/SystemComponentsModule.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.di import android.content.Context import android.content.pm.PackageManager +import android.location.LocationManager import com.duckduckgo.app.fire.FireAnimationLoader import com.duckduckgo.app.fire.LottieFireAnimationLoader import com.duckduckgo.app.global.shortcut.AppShortcutCreator @@ -47,6 +48,10 @@ object SystemComponentsModule { @Provides fun packageManager(context: Context): PackageManager = context.packageManager + @SingleInstanceIn(AppScope::class) + @Provides + fun locationManager(context: Context): LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + @SingleInstanceIn(AppScope::class) @Provides fun deviceAppsListProvider(packageManager: PackageManager): DeviceAppListProvider = InstalledDeviceAppListProvider(packageManager) diff --git a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt index 8e31b33560c4..132305e45741 100644 --- a/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt +++ b/app/src/main/java/com/duckduckgo/app/global/api/PixelParamRemovalInterceptor.kt @@ -29,6 +29,7 @@ import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelPa import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter.ATB import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter.OS_VERSION import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.site.permissions.impl.SitePermissionsPixelName import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import okhttp3.Interceptor @@ -87,6 +88,8 @@ object PixelInterceptorPixelsRequiringDataCleaning : PixelParamRemovalPlugin { HttpErrorPixelName.WEBVIEW_RECEIVED_HTTP_ERROR_4XX_DAILY.pixelName to PixelParameter.removeAtb(), HttpErrorPixelName.WEBVIEW_RECEIVED_HTTP_ERROR_5XX_DAILY.pixelName to PixelParameter.removeAtb(), AppPixelName.FEATURES_ENABLED_AT_SEARCH_TIME.pixelName to PixelParameter.removeAll(), + SitePermissionsPixelName.PERMISSION_DIALOG_CLICK.pixelName to PixelParameter.removeAtb(), + SitePermissionsPixelName.PERMISSION_DIALOG_IMPRESSION.pixelName to PixelParameter.removeAtb(), ) } } diff --git a/app/src/main/java/com/duckduckgo/app/global/migrations/LocationPermissionMigrationPlugin.kt b/app/src/main/java/com/duckduckgo/app/global/migrations/LocationPermissionMigrationPlugin.kt new file mode 100644 index 000000000000..4654d8360012 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/global/migrations/LocationPermissionMigrationPlugin.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.global.migrations + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.location.data.LocationPermissionType.ALLOW_ALWAYS +import com.duckduckgo.app.location.data.LocationPermissionType.DENY_ALWAYS +import com.duckduckgo.app.location.data.LocationPermissionsRepository +import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.plugins.migrations.MigrationPlugin +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest +import com.duckduckgo.site.permissions.impl.SitePermissionsRepository +import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@ContributesMultibinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class LocationPermissionMigrationPlugin @Inject constructor( + private val settingsDataStore: SettingsDataStore, + private val locationPermissionsRepository: LocationPermissionsRepository, + private val sitePermissionsRepository: SitePermissionsRepository, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) : MigrationPlugin { + + override val version: Int = 2 + + override fun run() { + appCoroutineScope.launch(dispatcherProvider.io()) { + if (!settingsDataStore.appLocationPermissionMigrated) { + sitePermissionsRepository.askLocationEnabled = settingsDataStore.appLocationPermission + Timber.d("Location permissions migrated: location permission set to ${sitePermissionsRepository.askLocationEnabled}") + val locationPermissions = locationPermissionsRepository.getLocationPermissionsSync() + val alwaysAllowedPermissions = locationPermissions.filter { it.permission == ALLOW_ALWAYS } + val alwaysDeniedPermissions = locationPermissions.filter { it.permission == DENY_ALWAYS } + alwaysAllowedPermissions.forEach { permission -> + sitePermissionsRepository.sitePermissionPermanentlySaved( + permission.domain, + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, + SitePermissionAskSettingType.ALLOW_ALWAYS, + ) + } + alwaysDeniedPermissions.forEach { permission -> + sitePermissionsRepository.sitePermissionPermanentlySaved( + permission.domain, + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, + SitePermissionAskSettingType.DENY_ALWAYS, + ) + } + settingsDataStore.appLocationPermissionMigrated = true + Timber.d("Location permissions migrated: ALLOW ALWAYS ${alwaysAllowedPermissions.size} DENY ALWAYS ${alwaysDeniedPermissions.size}.") + } + } + } +} diff --git a/app/src/main/java/com/duckduckgo/app/global/migrations/MigrationLifecycleObserver.kt b/app/src/main/java/com/duckduckgo/app/global/migrations/MigrationLifecycleObserver.kt index dd16f53af51a..31f1b27430fc 100644 --- a/app/src/main/java/com/duckduckgo/app/global/migrations/MigrationLifecycleObserver.kt +++ b/app/src/main/java/com/duckduckgo/app/global/migrations/MigrationLifecycleObserver.kt @@ -51,6 +51,6 @@ class MigrationLifecycleObserver @Inject constructor( } companion object { - const val CURRENT_VERSION = 1 + const val CURRENT_VERSION = 2 } } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index 4839651d9701..f334fde9ef21 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -278,16 +278,6 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName { SHORTCUT_ADDED("m_sho_a"), SHORTCUT_OPENED("m_sho_o"), - PRECISE_LOCATION_SYSTEM_DIALOG_ENABLE("m_pc_syd_e"), - PRECISE_LOCATION_SYSTEM_DIALOG_LATER("m_pc_syd_l"), - PRECISE_LOCATION_SYSTEM_DIALOG_NEVER("m_pc_syd_n"), - PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_ENABLE("m_pc_s_l_e"), - PRECISE_LOCATION_SETTINGS_LOCATION_PERMISSION_DISABLE("m_pc_s_l_d"), - PRECISE_LOCATION_SITE_DIALOG_ALLOW_ALWAYS("m_pc_sd_aa"), - PRECISE_LOCATION_SITE_DIALOG_ALLOW_ONCE("m_pc_sd_ao"), - PRECISE_LOCATION_SITE_DIALOG_DENY_ALWAYS("m_pc_sd_da"), - PRECISE_LOCATION_SITE_DIALOG_DENY_ONCE("m_pc_sd_do"), - FIRE_DIALOG_PROMOTED_CLEAR_PRESSED("m_fdp_p"), FIRE_DIALOG_CLEAR_PRESSED("m_fd_p"), FIRE_DIALOG_CANCEL("m_fd_c"), diff --git a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt index f643381b048b..d72ff1604765 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/db/SettingsDataStore.kt @@ -47,8 +47,20 @@ interface SettingsDataStore { @Deprecated(message = "Not used anymore after adding automatic fireproof", replaceWith = ReplaceWith(expression = "automaticFireproofSetting")) var appLoginDetection: Boolean var automaticFireproofSetting: AutomaticFireproofSetting + + @Deprecated( + message = "Not used anymore after migration to SitePermissionsRepository - https://app.asana.com/0/1174433894299346/1206170291275949/f", + replaceWith = ReplaceWith(expression = "SitePermissionsRepository.askLocationEnabled"), + ) var appLocationPermission: Boolean + + @Deprecated( + message = "Not used anymore after migration to SitePermissionsRepository - https://app.asana.com/0/1174433894299346/1206170291275949/f", + replaceWith = ReplaceWith(expression = "SitePermissionsRepository.askLocationEnabled"), + ) var appLocationPermissionDeniedForever: Boolean + var appLocationPermissionMigrated: Boolean + var globalPrivacyControlEnabled: Boolean var appLinksEnabled: Boolean var showAppLinksPrompt: Boolean @@ -121,6 +133,10 @@ class SettingsSharedPreferences @Inject constructor( get() = preferences.getBoolean(KEY_SYSTEM_LOCATION_PERMISSION_DENIED_FOREVER, false) set(enabled) = preferences.edit { putBoolean(KEY_SYSTEM_LOCATION_PERMISSION_DENIED_FOREVER, enabled) } + override var appLocationPermissionMigrated: Boolean + get() = preferences.getBoolean(KEY_SITE_LOCATION_PERMISSION_MIGRATED, false) + set(enabled) = preferences.edit { putBoolean(KEY_SITE_LOCATION_PERMISSION_MIGRATED, enabled) } + override var appIcon: AppIcon get() { val componentName = preferences.getString(KEY_APP_ICON, defaultIcon().componentName) ?: return defaultIcon() @@ -248,6 +264,7 @@ class SettingsSharedPreferences @Inject constructor( const val KEY_APP_ICON_CHANGED = "APP_ICON_CHANGED" const val KEY_SITE_LOCATION_PERMISSION_ENABLED = "KEY_SITE_LOCATION_PERMISSION_ENABLED" const val KEY_SYSTEM_LOCATION_PERMISSION_DENIED_FOREVER = "KEY_SYSTEM_LOCATION_PERMISSION_DENIED_FOREVER" + const val KEY_SITE_LOCATION_PERMISSION_MIGRATED = "KEY_SITE_LOCATION_PERMISSION_MIGRATED" const val KEY_DO_NOT_SELL_ENABLED = "KEY_DO_NOT_SELL_ENABLED" const val APP_LINKS_ENABLED = "APP_LINKS_ENABLED" const val SHOW_APP_LINKS_PROMPT = "SHOW_APP_LINKS_PROMPT" diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsActivity.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsActivity.kt index 796219e312d5..2ad5d5df52de 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsActivity.kt @@ -27,7 +27,6 @@ import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ActivitySitePermissionsBinding import com.duckduckgo.app.browser.favicon.FaviconManager -import com.duckduckgo.app.location.data.LocationPermissionEntity import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.LaunchWebsiteAllowed import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.ShowRemovedAllConfirmationSnackbar @@ -68,7 +67,7 @@ class SitePermissionsActivity : DuckDuckGoActivity() { viewModel.viewState .flowWithLifecycle(lifecycle, STARTED) .collectLatest { state -> - val sitePermissionsWebsites = viewModel.combineAllPermissions(state.locationPermissionsAllowed, state.sitesPermissionsAllowed) + val sitePermissionsWebsites = state.sitesPermissionsAllowed.map { it.domain } updateList(sitePermissionsWebsites, state.askLocationEnabled, state.askCameraEnabled, state.askMicEnabled, state.askDrmEnabled) } } @@ -81,14 +80,13 @@ class SitePermissionsActivity : DuckDuckGoActivity() { private fun processCommand(command: Command) { when (command) { - is ShowRemovedAllConfirmationSnackbar -> showRemovedAllSnackbar(command.removedSitePermissions, command.removedLocationPermissions) + is ShowRemovedAllConfirmationSnackbar -> showRemovedAllSnackbar(command.removedSitePermissions) is LaunchWebsiteAllowed -> launchWebsiteAllowed(command.domain) } } private fun showRemovedAllSnackbar( removedSitePermissions: List, - removedLocationPermissions: List, ) { val message = HtmlCompat.fromHtml(getString(R.string.sitePermissionsRemoveAllWebsitesSnackbarText), HtmlCompat.FROM_HTML_MODE_LEGACY) Snackbar.make( @@ -96,7 +94,7 @@ class SitePermissionsActivity : DuckDuckGoActivity() { message, Snackbar.LENGTH_LONG, ).setAction(R.string.fireproofWebsiteSnackbarAction) { - viewModel.onSnackBarUndoRemoveAllWebsites(removedSitePermissions, removedLocationPermissions) + viewModel.onSnackBarUndoRemoveAllWebsites(removedSitePermissions) }.show() } diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsAdapter.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsAdapter.kt index 84b408f965b9..0b1b08949e07 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsAdapter.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsAdapter.kt @@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.R.layout import com.duckduckgo.app.browser.databinding.ViewSitePermissionsDescriptionBinding import com.duckduckgo.app.browser.databinding.ViewSitePermissionsEmptyListBinding +import com.duckduckgo.app.browser.databinding.ViewSitePermissionsSiteBinding import com.duckduckgo.app.browser.databinding.ViewSitePermissionsTitleBinding import com.duckduckgo.app.browser.databinding.ViewSitePermissionsToggleBinding import com.duckduckgo.app.browser.favicon.FaviconManager @@ -49,7 +50,6 @@ import com.duckduckgo.common.ui.menu.PopupMenu import com.duckduckgo.common.ui.view.PopupMenuItemView import com.duckduckgo.common.ui.view.divider.HorizontalDivider import com.duckduckgo.common.ui.view.setEnabledOpacity -import com.duckduckgo.mobile.android.databinding.RowOneLineListItemBinding import kotlinx.coroutines.launch class SitePermissionsAdapter( @@ -96,24 +96,29 @@ class SitePermissionsAdapter( val binding = ViewSitePermissionsDescriptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) SitePermissionsSimpleViewHolder(binding) } + HEADER -> { val binding = ViewSitePermissionsTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false) SitePermissionsHeaderViewHolder(binding, LayoutInflater.from(parent.context), viewModel) } + TOGGLE -> { val binding = ViewSitePermissionsToggleBinding.inflate(LayoutInflater.from(parent.context), parent, false) SitePermissionToggleViewHolder(binding) } + DIVIDER -> { val view = HorizontalDivider(parent.context) SitePermissionsDividerViewHolder(view) } + SITES_EMPTY -> { val binding = ViewSitePermissionsEmptyListBinding.inflate(LayoutInflater.from(parent.context), parent, false) SitePermissionsSimpleViewHolder(binding) } + SITE_ALLOWED_ITEM -> { - val binding = RowOneLineListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = ViewSitePermissionsSiteBinding.inflate(LayoutInflater.from(parent.context), parent, false) SiteViewHolder(binding, viewModel, lifecycleOwner, faviconManager) } } @@ -127,6 +132,7 @@ class SitePermissionsAdapter( is SitePermissionToggle -> (holder as SitePermissionToggleViewHolder).bind(item) { _, isChecked -> viewModel.permissionToggleSelected(isChecked, item.text) } + is SiteAllowedItem -> (holder as SiteViewHolder).bind(item) else -> {} } @@ -159,6 +165,7 @@ class SitePermissionsAdapter( setOnClickListener { showOverflowMenu(isListEmpty) } } } + else -> binding.sitePermissionsSectionHeader.showOverflowMenuIcon(false) } binding.sitePermissionsSectionHeader.setText(title) @@ -194,6 +201,7 @@ class SitePermissionsAdapter( R.drawable.ic_location_blocked_24 } } + R.string.sitePermissionsSettingsCamera -> { if (item.enable) { R.drawable.ic_video_24 @@ -201,6 +209,7 @@ class SitePermissionsAdapter( R.drawable.ic_video_blocked_24 } } + R.string.sitePermissionsSettingsMicrophone -> { if (item.enable) { R.drawable.ic_microphone_24 @@ -208,6 +217,7 @@ class SitePermissionsAdapter( R.drawable.ic_microphone_blocked_24 } } + R.string.sitePermissionsSettingsDRM -> { if (item.enable) { R.drawable.ic_video_player_24 @@ -215,6 +225,7 @@ class SitePermissionsAdapter( R.drawable.ic_video_player_blocked_24 } } + else -> null } iconRes?.let { @@ -226,7 +237,7 @@ class SitePermissionsAdapter( } class SiteViewHolder( - private val binding: RowOneLineListItemBinding, + private val binding: ViewSitePermissionsSiteBinding, private val viewModel: SitePermissionsViewModel, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager, diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModel.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModel.kt index 7193b41690b9..9289746bf2bd 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModel.kt @@ -20,10 +20,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.R -import com.duckduckgo.app.location.GeoLocationPermissions -import com.duckduckgo.app.location.data.LocationPermissionEntity -import com.duckduckgo.app.location.data.LocationPermissionsRepository -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.LaunchWebsiteAllowed import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.ShowRemovedAllConfirmationSnackbar import com.duckduckgo.common.utils.DispatcherProvider @@ -36,16 +32,12 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @ContributesViewModel(ActivityScope::class) class SitePermissionsViewModel @Inject constructor( private val sitePermissionsRepository: SitePermissionsRepository, - private val locationPermissionsRepository: LocationPermissionsRepository, - private val geolocationPermissions: GeoLocationPermissions, - private val settingsDataStore: SettingsDataStore, private val dispatcherProvider: DispatcherProvider, ) : ViewModel() { @@ -63,20 +55,17 @@ class SitePermissionsViewModel @Inject constructor( val askMicEnabled: Boolean = true, val askDrmEnabled: Boolean = true, val sitesPermissionsAllowed: List = listOf(), - val locationPermissionsAllowed: List = listOf(), ) sealed class Command { - class ShowRemovedAllConfirmationSnackbar( - val removedSitePermissions: List, - val removedLocationPermissions: List, - ) : Command() + class ShowRemovedAllConfirmationSnackbar(val removedSitePermissions: List) : Command() class LaunchWebsiteAllowed(val domain: String) : Command() } init { _viewState.value = ViewState( - askLocationEnabled = settingsDataStore.appLocationPermission, + // askLocationEnabled = settingsDataStore.appLocationPermission, + askLocationEnabled = sitePermissionsRepository.askLocationEnabled, askCameraEnabled = sitePermissionsRepository.askCameraEnabled, askMicEnabled = sitePermissionsRepository.askMicEnabled, askDrmEnabled = sitePermissionsRepository.askDrmEnabled, @@ -85,43 +74,36 @@ class SitePermissionsViewModel @Inject constructor( fun allowedSites() { viewModelScope.launch { - val locationsPermissionsFlow = locationPermissionsRepository.getLocationPermissionsFlow() - val sitePermissionsFlow = sitePermissionsRepository.sitePermissionsWebsitesFlow() - - sitePermissionsFlow.combine(locationsPermissionsFlow) { sitePermissionsList, locationPermissionsList -> - Pair(sitePermissionsList, locationPermissionsList) - }.collect { + sitePermissionsRepository.sitePermissionsWebsitesFlow().collect { _viewState.emit( _viewState.value.copy( - sitesPermissionsAllowed = it.first, - locationPermissionsAllowed = it.second, + sitesPermissionsAllowed = it, ), ) } } } - fun combineAllPermissions(locationPermissions: List, sitePermissions: List): List = - locationPermissions.map { it.domain }.union(sitePermissions.map { it.domain }).toList() - fun permissionToggleSelected( isChecked: Boolean, textRes: Int, ) { when (textRes) { R.string.sitePermissionsSettingsLocation -> { - settingsDataStore.appLocationPermission = isChecked + sitePermissionsRepository.askLocationEnabled = isChecked _viewState.value = _viewState.value.copy(askLocationEnabled = isChecked) - removeLocationSites() } + R.string.sitePermissionsSettingsCamera -> { sitePermissionsRepository.askCameraEnabled = isChecked _viewState.value = _viewState.value.copy(askCameraEnabled = isChecked) } + R.string.sitePermissionsSettingsMicrophone -> { sitePermissionsRepository.askMicEnabled = isChecked _viewState.value = _viewState.value.copy(askMicEnabled = isChecked) } + R.string.sitePermissionsSettingsDRM -> { sitePermissionsRepository.askDrmEnabled = isChecked _viewState.value = _viewState.value.copy(askDrmEnabled = isChecked) @@ -129,12 +111,6 @@ class SitePermissionsViewModel @Inject constructor( } } - private fun removeLocationSites() { - viewModelScope.launch { - geolocationPermissions.clearAll() - } - } - fun allowedSiteSelected(domain: String) { viewModelScope.launch { _commands.send(LaunchWebsiteAllowed(domain)) @@ -143,24 +119,18 @@ class SitePermissionsViewModel @Inject constructor( fun removeAllSitesSelected() { val sitePermissions = _viewState.value.sitesPermissionsAllowed.toMutableList() - val locationPermissions = _viewState.value.locationPermissionsAllowed.toMutableList() viewModelScope.launch(dispatcherProvider.io()) { sitePermissionsRepository.sitePermissionsAllowedFlow().collect { sitePermissionsAllowed -> - geolocationPermissions.clearAll() sitePermissionsRepository.deleteAll() - _commands.send(ShowRemovedAllConfirmationSnackbar(sitePermissions, locationPermissions)) + _commands.send(ShowRemovedAllConfirmationSnackbar(sitePermissions)) cachedAllowedSites = sitePermissionsAllowed } } } - fun onSnackBarUndoRemoveAllWebsites( - removedSitePermissions: List, - removedLocationPermissions: List, - ) { + fun onSnackBarUndoRemoveAllWebsites(removedSitePermissions: List) { viewModelScope.launch(dispatcherProvider.io()) { sitePermissionsRepository.undoDeleteAll(removedSitePermissions, cachedAllowedSites) - geolocationPermissions.undoClearAll(removedLocationPermissions) } } } diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteActivity.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteActivity.kt index b86dcd6230d0..8d2c6512f021 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteActivity.kt @@ -41,6 +41,7 @@ import com.duckduckgo.common.utils.extensions.websiteFromGeoLocationsApiOrigin import com.duckduckgo.di.scopes.ActivityScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import timber.log.Timber @InjectWith(ActivityScope::class) class PermissionsPerWebsiteActivity : DuckDuckGoActivity() { @@ -135,6 +136,8 @@ class PermissionsPerWebsiteActivity : DuckDuckGoActivity() { override fun onPositiveButtonClicked(selectedItem: Int) { val permissionSettingSelected = selectedItem.getPermissionSettingOptionFromPosition() val newPermissionSetting = WebsitePermissionSetting(currentOption.icon, currentOption.title, permissionSettingSelected) + Timber.d("Permissions: permissionSettingSelected $permissionSettingSelected") + Timber.d("Permissions: newPermissionSetting $newPermissionSetting") viewModel.onPermissionSettingSelected(newPermissionSetting, url) } }, diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteViewModel.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteViewModel.kt index 234ebade639d..8795c02116d2 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/PermissionsPerWebsiteViewModel.kt @@ -20,16 +20,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.R -import com.duckduckgo.app.location.data.LocationPermissionEntity -import com.duckduckgo.app.location.data.LocationPermissionType -import com.duckduckgo.app.location.data.LocationPermissionsRepository -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.sitepermissions.permissionsperwebsite.PermissionsPerWebsiteViewModel.Command.GoBackToSitePermissions import com.duckduckgo.app.sitepermissions.permissionsperwebsite.PermissionsPerWebsiteViewModel.Command.ShowPermissionSettingSelectionDialog -import com.duckduckgo.app.sitepermissions.permissionsperwebsite.WebsitePermissionSettingOption.ALLOW import com.duckduckgo.app.sitepermissions.permissionsperwebsite.WebsitePermissionSettingOption.ASK import com.duckduckgo.app.sitepermissions.permissionsperwebsite.WebsitePermissionSettingOption.ASK_DISABLED -import com.duckduckgo.app.sitepermissions.permissionsperwebsite.WebsitePermissionSettingOption.DENY import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.site.permissions.impl.SitePermissionsRepository import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionsEntity @@ -40,12 +34,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import timber.log.Timber @ContributesViewModel(ActivityScope::class) class PermissionsPerWebsiteViewModel @Inject constructor( private val sitePermissionsRepository: SitePermissionsRepository, - private val locationPermissionsRepository: LocationPermissionsRepository, - private val settingsDataStore: SettingsDataStore, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -66,8 +59,9 @@ class PermissionsPerWebsiteViewModel @Inject constructor( fun websitePermissionSettings(url: String) { viewModelScope.launch { val websitePermissionsSettings = sitePermissionsRepository.getSitePermissionsForWebsite(url) - val locationSetting = locationPermissionsRepository.getDomainPermission(url) - val websitePermissions = convertToWebsitePermissionSettings(websitePermissionsSettings, locationSetting) + val websitePermissions = convertToWebsitePermissionSettings(websitePermissionsSettings) + Timber.d("Permissions: websitePermissionsSettings for $url $websitePermissionsSettings") + Timber.d("Permissions: websitePermissions for $url $websitePermissions") _viewState.value = _viewState.value.copy(websitePermissions = websitePermissions) } @@ -75,10 +69,9 @@ class PermissionsPerWebsiteViewModel @Inject constructor( private fun convertToWebsitePermissionSettings( sitePermissionsEntity: SitePermissionsEntity?, - locationPermissionEntity: LocationPermissionEntity?, ): List { - var locationSetting = WebsitePermissionSettingOption.mapToWebsitePermissionSetting(locationPermissionEntity?.permission?.name) - if (locationSetting == ASK && !settingsDataStore.appLocationPermission) { + var locationSetting = WebsitePermissionSettingOption.mapToWebsitePermissionSetting(sitePermissionsEntity?.askLocationSetting) + if (locationSetting == ASK && !sitePermissionsRepository.askLocationEnabled) { locationSetting = ASK_DISABLED } @@ -143,7 +136,10 @@ class PermissionsPerWebsiteViewModel @Inject constructor( } } - fun onPermissionSettingSelected(editedPermissionSetting: WebsitePermissionSetting, url: String) { + fun onPermissionSettingSelected( + editedPermissionSetting: WebsitePermissionSetting, + url: String, + ) { var askLocationSetting = viewState.value.websitePermissions[0].setting var askCameraSetting = viewState.value.websitePermissions[1].setting var askMicSetting = viewState.value.websitePermissions[2].setting @@ -151,55 +147,46 @@ class PermissionsPerWebsiteViewModel @Inject constructor( when (editedPermissionSetting.title) { R.string.sitePermissionsSettingsLocation -> { - askLocationSetting = when (editedPermissionSetting.setting == ASK && !settingsDataStore.appLocationPermission) { + askLocationSetting = when (editedPermissionSetting.setting == ASK && !sitePermissionsRepository.askLocationEnabled) { true -> ASK_DISABLED false -> editedPermissionSetting.setting } - updateLocationSetting(editedPermissionSetting.setting, url) } + R.string.sitePermissionsSettingsCamera -> { askCameraSetting = when (editedPermissionSetting.setting == ASK && !sitePermissionsRepository.askCameraEnabled) { true -> ASK_DISABLED false -> editedPermissionSetting.setting } - updateSitePermissionsSetting(askCameraSetting, askMicSetting, askDrmSetting, url) } + R.string.sitePermissionsSettingsMicrophone -> { askMicSetting = when (editedPermissionSetting.setting == ASK && !sitePermissionsRepository.askMicEnabled) { true -> ASK_DISABLED false -> editedPermissionSetting.setting } - updateSitePermissionsSetting(askCameraSetting, askMicSetting, askDrmSetting, url) } + R.string.sitePermissionsSettingsDRM -> { askDrmSetting = when (editedPermissionSetting.setting == ASK && !sitePermissionsRepository.askDrmEnabled) { true -> ASK_DISABLED false -> editedPermissionSetting.setting } - updateSitePermissionsSetting(askCameraSetting, askMicSetting, askDrmSetting, url) } } + updateSitePermissionsSetting(askCameraSetting, askMicSetting, askDrmSetting, askLocationSetting, url) + _viewState.value = _viewState.value.copy( websitePermissions = getSettingsList(askLocationSetting, askCameraSetting, askMicSetting, askDrmSetting), ) } - private fun updateLocationSetting(locationSetting: WebsitePermissionSettingOption, url: String) { - val locationPermissionType = when (locationSetting) { - ASK, ASK_DISABLED -> LocationPermissionType.ALLOW_ONCE - DENY -> LocationPermissionType.DENY_ALWAYS - ALLOW -> LocationPermissionType.ALLOW_ALWAYS - } - viewModelScope.launch { - locationPermissionsRepository.savePermission(url, locationPermissionType) - } - } - private fun updateSitePermissionsSetting( askCameraSetting: WebsitePermissionSettingOption, askMicSetting: WebsitePermissionSettingOption, askDrmSetting: WebsitePermissionSettingOption, + askLocationSetting: WebsitePermissionSettingOption, url: String, ) { val sitePermissionsEntity = SitePermissionsEntity( @@ -207,6 +194,7 @@ class PermissionsPerWebsiteViewModel @Inject constructor( askCameraSetting = askCameraSetting.toSitePermissionSettingEntityType().name, askMicSetting = askMicSetting.toSitePermissionSettingEntityType().name, askDrmSetting = askDrmSetting.toSitePermissionSettingEntityType().name, + askLocationSetting = askLocationSetting.toSitePermissionSettingEntityType().name, ) viewModelScope.launch { sitePermissionsRepository.savePermission(sitePermissionsEntity) diff --git a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/WebsitePermissionSettingOption.kt b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/WebsitePermissionSettingOption.kt index 84f26e93cb19..819cf306eca1 100644 --- a/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/WebsitePermissionSettingOption.kt +++ b/app/src/main/java/com/duckduckgo/app/sitepermissions/permissionsperwebsite/WebsitePermissionSettingOption.kt @@ -53,13 +53,7 @@ enum class WebsitePermissionSettingOption( } fun Int.getPermissionSettingOptionFromPosition(): WebsitePermissionSettingOption { - var option = ASK - values().forEach { - if (it.order == this) { - option = it - } - } - return option + return entries.first { it.order == this } } } } diff --git a/app/src/main/res/layout/view_site_permissions_site.xml b/app/src/main/res/layout/view_site_permissions_site.xml new file mode 100644 index 000000000000..f84397a003cd --- /dev/null +++ b/app/src/main/res/layout/view_site_permissions_site.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4635a04ddbd..db9aef527b48 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -546,8 +546,8 @@ Maybe Later Don\'t Ask Again for This Site Grant %1$s permission to access location? - We only use your anonymous location to deliver better results, closer to you. You can always change your mind later. You can manage the location access permissions you’ve granted to individual sites in Settings. + We only use your anonymous location to deliver better results, closer to you. You can manage the location access permissions you’ve granted to individual sites in Settings. Always Only for This Session Deny Always @@ -672,10 +672,10 @@ No sites yet Permissions Removed for All Sites Permissions for \"%1$s\" - Ask + Ask every time Deny Allow - \"Ask\" disabled for all sites + Disabled for all sites %1$s permission for %2$s diff --git a/app/src/test/java/com/duckduckgo/app/Fakes.kt b/app/src/test/java/com/duckduckgo/app/Fakes.kt index b8720c50c6a1..a882a653e03f 100644 --- a/app/src/test/java/com/duckduckgo/app/Fakes.kt +++ b/app/src/test/java/com/duckduckgo/app/Fakes.kt @@ -58,6 +58,10 @@ class FakeSettingsDataStore : SettingsDataStore { get() = store["appLocationPermissionDeniedForever"] as Boolean? ?: false set(value) { store["appLocationPermissionDeniedForever"] = value } + override var appLocationPermissionMigrated: Boolean + get() = store["appLocationPermissionMigrated"] as Boolean? ?: false + set(value) { store["appLocationPermissionMigrated"] = value } + override var appIcon: AppIcon get() = store["appIcon"] as AppIcon? ?: defaultIcon() set(value) { store["appIcon"] = value } diff --git a/app/src/test/java/com/duckduckgo/app/global/migrations/LocationPermissionsMigrationPluginTest.kt b/app/src/test/java/com/duckduckgo/app/global/migrations/LocationPermissionsMigrationPluginTest.kt new file mode 100644 index 000000000000..1e87049f55cc --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/global/migrations/LocationPermissionsMigrationPluginTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.global.migrations + +import com.duckduckgo.app.location.data.LocationPermissionEntity +import com.duckduckgo.app.location.data.LocationPermissionType.ALLOW_ALWAYS +import com.duckduckgo.app.location.data.LocationPermissionType.DENY_ALWAYS +import com.duckduckgo.app.location.data.LocationPermissionsRepository +import com.duckduckgo.app.settings.db.SettingsDataStore +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest +import com.duckduckgo.site.permissions.impl.SitePermissionsRepository +import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType +import kotlinx.coroutines.test.TestScope +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +class LocationPermissionsMigrationPluginTest { + + @get:Rule + var coroutineRule = CoroutineTestRule() + + private var settingsDataStore: SettingsDataStore = mock() + private var locationPermissionsRepository: LocationPermissionsRepository = mock() + private var sitePermissionsRepository: SitePermissionsRepository = mock() + + private lateinit var testee: LocationPermissionMigrationPlugin + + @Before + fun before() { + testee = LocationPermissionMigrationPlugin( + settingsDataStore, + locationPermissionsRepository, + sitePermissionsRepository, + TestScope(), + coroutineRule.testDispatcherProvider, + ) + } + + @Test + fun whenMigrationIsNeededAndRanThenMigrationStateStored() { + whenever(settingsDataStore.appLocationPermissionMigrated).thenReturn(false) + + testee.run() + + verify(settingsDataStore).appLocationPermissionMigrated = true + } + + @Test + fun whenMigrationIsNeededAndRanThenLocationPermissionMigrated() { + whenever(settingsDataStore.appLocationPermissionMigrated).thenReturn(false) + whenever(settingsDataStore.appLocationPermission).thenReturn(false) + + testee.run() + + verify(sitePermissionsRepository).askLocationEnabled = false + } + + @Test + fun whenMigrationNotNeededAThenNothingHappens() { + whenever(settingsDataStore.appLocationPermissionMigrated).thenReturn(true) + + testee.run() + + verifyNoInteractions(locationPermissionsRepository) + } + + @Test + fun whenAllowedPermissionsPresentThenCanBeMigrated() { + whenever(settingsDataStore.appLocationPermissionMigrated).thenReturn(false) + whenever(locationPermissionsRepository.getLocationPermissionsSync()).thenReturn( + listOf(LocationPermissionEntity("domain.com", ALLOW_ALWAYS)), + ) + + testee.run() + + verify(sitePermissionsRepository).sitePermissionPermanentlySaved( + "domain.com", + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, + SitePermissionAskSettingType.ALLOW_ALWAYS, + ) + } + + @Test + fun whenDeniedPermissionsPresentThenCanBeMigrated() { + whenever(settingsDataStore.appLocationPermissionMigrated).thenReturn(false) + whenever(locationPermissionsRepository.getLocationPermissionsSync()).thenReturn( + listOf(LocationPermissionEntity("domain.com", DENY_ALWAYS)), + ) + + testee.run() + + verify(sitePermissionsRepository).sitePermissionPermanentlySaved( + "domain.com", + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, + SitePermissionAskSettingType.DENY_ALWAYS, + ) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionEntityTest.kt b/app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionRequestEntityTest.kt similarity index 97% rename from app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionEntityTest.kt rename to app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionRequestEntityTest.kt index 9705b0a62b75..30c712b21590 100644 --- a/app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionEntityTest.kt +++ b/app/src/test/java/com/duckduckgo/app/location/data/LocationPermissionRequestEntityTest.kt @@ -20,7 +20,7 @@ import com.duckduckgo.common.utils.extensions.asLocationPermissionOrigin import org.junit.Assert import org.junit.Test -class LocationPermissionEntityTest { +class LocationPermissionRequestEntityTest { @Test fun whenDomainStartsWithHttpsThenDropPrefix() { diff --git a/app/src/test/java/com/duckduckgo/app/sitepermissions/PermissionsPerWebsiteViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/sitepermissions/PermissionsPerWebsiteViewModelTest.kt index d744f3881010..5895875cf281 100644 --- a/app/src/test/java/com/duckduckgo/app/sitepermissions/PermissionsPerWebsiteViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/sitepermissions/PermissionsPerWebsiteViewModelTest.kt @@ -18,10 +18,6 @@ package com.duckduckgo.app.sitepermissions import app.cash.turbine.test import com.duckduckgo.app.browser.R -import com.duckduckgo.app.location.data.LocationPermissionEntity -import com.duckduckgo.app.location.data.LocationPermissionType -import com.duckduckgo.app.location.data.LocationPermissionsRepository -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.sitepermissions.permissionsperwebsite.PermissionsPerWebsiteViewModel import com.duckduckgo.app.sitepermissions.permissionsperwebsite.PermissionsPerWebsiteViewModel.Command.GoBackToSitePermissions import com.duckduckgo.app.sitepermissions.permissionsperwebsite.PermissionsPerWebsiteViewModel.Command.ShowPermissionSettingSelectionDialog @@ -47,13 +43,9 @@ class PermissionsPerWebsiteViewModelTest { var coroutineRule = CoroutineTestRule() private val mockSitePermissionsRepository: SitePermissionsRepository = mock() - private val mockLocationPermissionsRepository: LocationPermissionsRepository = mock() - private val mockSettingsDataStore: SettingsDataStore = mock() private val viewModel = PermissionsPerWebsiteViewModel( sitePermissionsRepository = mockSitePermissionsRepository, - locationPermissionsRepository = mockLocationPermissionsRepository, - settingsDataStore = mockSettingsDataStore, ) private val domain = "domain.com" @@ -211,20 +203,21 @@ class PermissionsPerWebsiteViewModelTest { micEnabled: Boolean = true, cameraEnabled: Boolean = true, locationEnabled: Boolean = true, + drmEnabled: Boolean = true, ) { - whenever(mockSettingsDataStore.appLocationPermission).thenReturn(locationEnabled) whenever(mockSitePermissionsRepository.askMicEnabled).thenReturn(micEnabled) whenever(mockSitePermissionsRepository.askCameraEnabled).thenReturn(cameraEnabled) + whenever(mockSitePermissionsRepository.askDrmEnabled).thenReturn(drmEnabled) + whenever(mockSitePermissionsRepository.askLocationEnabled).thenReturn(locationEnabled) } private fun loadWebsitePermissionsSettings( cameraSetting: String = SitePermissionAskSettingType.ASK_EVERY_TIME.name, micSetting: String = SitePermissionAskSettingType.ASK_EVERY_TIME.name, - locationSetting: LocationPermissionType = LocationPermissionType.ALLOW_ALWAYS, + locationSetting: String = SitePermissionAskSettingType.ASK_EVERY_TIME.name, + drmSetting: String = SitePermissionAskSettingType.ASK_EVERY_TIME.name, ) { - val testLocationEntity = LocationPermissionEntity(domain, locationSetting) - val testSitePermissionEntity = SitePermissionsEntity(domain, cameraSetting, micSetting) + val testSitePermissionEntity = SitePermissionsEntity(domain, cameraSetting, micSetting, drmSetting, locationSetting) mockSitePermissionsRepository.stub { onBlocking { getSitePermissionsForWebsite(domain) }.thenReturn(testSitePermissionEntity) } - mockLocationPermissionsRepository.stub { onBlocking { getDomainPermission(domain) }.thenReturn(testLocationEntity) } } } diff --git a/app/src/test/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModelTest.kt index 9287b8a746d1..49d4d44646ea 100644 --- a/app/src/test/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/sitepermissions/SitePermissionsViewModelTest.kt @@ -18,11 +18,6 @@ package com.duckduckgo.app.sitepermissions import app.cash.turbine.test import com.duckduckgo.app.browser.R -import com.duckduckgo.app.location.GeoLocationPermissions -import com.duckduckgo.app.location.data.LocationPermissionEntity -import com.duckduckgo.app.location.data.LocationPermissionType -import com.duckduckgo.app.location.data.LocationPermissionsRepository -import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.LaunchWebsiteAllowed import com.duckduckgo.app.sitepermissions.SitePermissionsViewModel.Command.ShowRemovedAllConfirmationSnackbar import com.duckduckgo.common.test.CoroutineTestRule @@ -46,15 +41,9 @@ class SitePermissionsViewModelTest { var coroutineRule = CoroutineTestRule() private val mockSitePermissionsRepository: SitePermissionsRepository = mock() - private val mockLocationPermissionsRepository: LocationPermissionsRepository = mock() - private val mockGeoLocationPermissions: GeoLocationPermissions = mock() - private val mockSettingsDataStore: SettingsDataStore = mock() private val viewModel = SitePermissionsViewModel( sitePermissionsRepository = mockSitePermissionsRepository, - locationPermissionsRepository = mockLocationPermissionsRepository, - geolocationPermissions = mockGeoLocationPermissions, - settingsDataStore = mockSettingsDataStore, dispatcherProvider = coroutineRule.testDispatcherProvider, ) @@ -74,25 +63,15 @@ class SitePermissionsViewModelTest { } @Test - fun whenAllowedSitesLoadedThenViewStateEmittedLocationWebsites() = runTest { + fun whenRemoveAllWebsitesThenDeleteAllSitePermissionsIsCalled() = runTest { viewModel.viewState.test { - val sitePermissions = awaitItem().locationPermissionsAllowed - assertEquals(1, sitePermissions.size) - } - } - - @Test - fun whenRemoveAllWebsitesThenClearAllLocationWebsitesIsCalled() = runTest { - viewModel.removeAllSitesSelected() + viewModel.removeAllSitesSelected() - verify(mockGeoLocationPermissions).clearAll() - } + verify(mockSitePermissionsRepository).deleteAll() - @Test - fun whenRemoveAllWebsitesThenDeleteAllSitePermissionsIsCalled() = runTest { - viewModel.removeAllSitesSelected() - - verify(mockSitePermissionsRepository).deleteAll() + val sitePermissions = expectMostRecentItem().sitesPermissionsAllowed + assertEquals(2, sitePermissions.size) + } } @Test @@ -163,6 +142,16 @@ class SitePermissionsViewModelTest { } } + @Test + fun whenToggleOffAskForDRMThenViewStateEmitted() = runTest { + viewModel.permissionToggleSelected(false, R.string.sitePermissionsSettingsDRM) + + viewModel.viewState.test { + val drmEnabled = awaitItem().askDrmEnabled + assertFalse(drmEnabled) + } + } + @Test fun whenWebsiteIsTappedThenNavigateToPermissionsPerWebsiteScreen() = runTest { val testDomain = "website1.com" @@ -174,16 +163,20 @@ class SitePermissionsViewModelTest { } private fun loadWebsites() { - val locationPermissions = listOf(LocationPermissionEntity("www.website1.com", LocationPermissionType.ALLOW_ONCE)) val sitePermissions = listOf(SitePermissionsEntity("www.website2.com"), SitePermissionsEntity("www.website3.com")) - whenever(mockLocationPermissionsRepository.getLocationPermissionsFlow()).thenReturn(flowOf(locationPermissions)) whenever(mockSitePermissionsRepository.sitePermissionsWebsitesFlow()).thenReturn(flowOf(sitePermissions)) whenever(mockSitePermissionsRepository.sitePermissionsAllowedFlow()).thenReturn(flowOf(emptyList())) } - private fun loadPermissionsSettings(micEnabled: Boolean = true, cameraEnabled: Boolean = true, locationEnabled: Boolean = true) { - whenever(mockSettingsDataStore.appLocationPermission).thenReturn(locationEnabled) + private fun loadPermissionsSettings( + micEnabled: Boolean = true, + cameraEnabled: Boolean = true, + locationEnabled: Boolean = true, + drmEnabled: Boolean = true, + ) { whenever(mockSitePermissionsRepository.askMicEnabled).thenReturn(micEnabled) whenever(mockSitePermissionsRepository.askCameraEnabled).thenReturn(cameraEnabled) + whenever(mockSitePermissionsRepository.askLocationEnabled).thenReturn(locationEnabled) + whenever(mockSitePermissionsRepository.askDrmEnabled).thenReturn(drmEnabled) } } diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/dialog/TextAlertDialogBuilder.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/dialog/TextAlertDialogBuilder.kt index 5faeaa1f0a96..fc7850d47afb 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/dialog/TextAlertDialogBuilder.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/dialog/TextAlertDialogBuilder.kt @@ -17,13 +17,23 @@ package com.duckduckgo.common.ui.view.dialog import android.content.Context +import android.text.Annotation +import android.text.SpannableString +import android.text.Spanned +import android.text.SpannedString +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.UnderlineSpan import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import com.duckduckgo.common.ui.view.button.ButtonType import com.duckduckgo.common.ui.view.button.DaxButton +import com.duckduckgo.common.ui.view.getColorFromAttr import com.duckduckgo.common.ui.view.gone import com.duckduckgo.common.ui.view.show import com.duckduckgo.mobile.android.R @@ -48,6 +58,7 @@ class TextAlertDialogBuilder(val context: Context) : DaxAlertDialog { private var listener: EventListener = DefaultEventListener() private var titleText: CharSequence = "" private var messageText: CharSequence = "" + private var messageClickable: Boolean = false private var headerImageDrawableId = 0 private var positiveButtonText: CharSequence = "" private var positiveButtonType: ButtonType = ButtonType.PRIMARY @@ -73,6 +84,47 @@ class TextAlertDialogBuilder(val context: Context) : DaxAlertDialog { return this } + fun setClickableMessage(textSequence: CharSequence, annotation: String, onClick: () -> Unit): TextAlertDialogBuilder { + val fullText = textSequence as SpannedString + val spannableString = SpannableString(fullText) + val annotations = fullText.getSpans(0, fullText.length, Annotation::class.java) + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + onClick() + } + } + + annotations?.find { it.value == annotation }?.let { + spannableString.apply { + setSpan( + clickableSpan, + fullText.getSpanStart(it), + fullText.getSpanEnd(it), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + setSpan( + UnderlineSpan(), + fullText.getSpanStart(it), + fullText.getSpanEnd(it), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + setSpan( + ForegroundColorSpan( + context.getColorFromAttr(R.attr.daxColorAccentBlue), + ), + fullText.getSpanStart(it), + fullText.getSpanEnd(it), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } + } + + messageText = spannableString + messageClickable = true + + return this + } + fun setTitle(text: CharSequence): TextAlertDialogBuilder { titleText = text return this @@ -169,6 +221,9 @@ class TextAlertDialogBuilder(val context: Context) : DaxAlertDialog { binding.textAlertDialogMessage.gone() } else { binding.textAlertDialogMessage.text = messageText + if (messageClickable) { + binding.textAlertDialogMessage.movementMethod = LinkMovementMethod.getInstance() + } } setButtons(binding, dialog) diff --git a/common/common-ui/src/main/res/color/text_input_color_selector.xml b/common/common-ui/src/main/res/color/text_input_color_selector.xml index e5a68c72aeff..db2d3e3d8831 100644 --- a/common/common-ui/src/main/res/color/text_input_color_selector.xml +++ b/common/common-ui/src/main/res/color/text_input_color_selector.xml @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/common/common-ui/src/main/res/layout/dialog_text_alert.xml b/common/common-ui/src/main/res/layout/dialog_text_alert.xml index 706a28bacab1..2f4c4588ae8c 100644 --- a/common/common-ui/src/main/res/layout/dialog_text_alert.xml +++ b/common/common-ui/src/main/res/layout/dialog_text_alert.xml @@ -16,7 +16,7 @@ ~ limitations under the License. --> - - + app:layout_constraintTop_toTopOf="parent"> - + - + + + + + + + + + + + + + - - - \ No newline at end of file + \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 24f13819d051..c57356ffaa95 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -124,6 +124,7 @@ platform :android do options_release_number = options[:release_number] options_release_notes = options[:release_notes] + options_release_notes = options[:release_notes] options_notes_type = options[:notes_type] newVersion = determine_version_number( diff --git a/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsDialogLauncher.kt b/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsDialogLauncher.kt index 6308117364b5..da2999f06c88 100644 --- a/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsDialogLauncher.kt +++ b/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsDialogLauncher.kt @@ -18,7 +18,7 @@ interface SitePermissionsDialogLauncher { fun registerPermissionLauncher(caller: ActivityResultCaller) /** - * This method should be called if website requests site permissions (audio or video). It will launch dialogs flow for asking the user. + * This method should be called if website requests site permissions (audio, video, location or DRM). It will launch dialogs flow for asking the user. * * @param activity where this method is called from * @param url URL taken from the permissions request object diff --git a/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsManager.kt b/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsManager.kt index d1ec96e26736..3905981a31d8 100644 --- a/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsManager.kt +++ b/site-permissions/site-permissions-api/src/main/java/com/duckduckgo/site/permissions/api/SitePermissionsManager.kt @@ -16,7 +16,10 @@ package com.duckduckgo.site.permissions.api +import android.net.Uri +import android.webkit.GeolocationPermissions import android.webkit.PermissionRequest +import androidx.core.net.toUri /** Public interface for managing site permissions data */ interface SitePermissionsManager { @@ -47,6 +50,15 @@ interface SitePermissionsManager { */ suspend fun getPermissionsQueryResponse(url: String, tabId: String, queriedPermission: String): SitePermissionQueryResponse + /** + * Checks if a site has been granted a specific set of permissions permanently + * + * @param url website querying the permission + * @param request original permission request type + * @return Returns true if site has that type of permission enabled + */ + suspend fun hasSitePermanentPermission(url: String, request: String): Boolean + data class SitePermissions( val autoAccept: List, val userHandled: List, @@ -61,4 +73,34 @@ interface SitePermissionsManager { object Prompt : SitePermissionQueryResponse() object Denied : SitePermissionQueryResponse() } + + /** + * Class that represents a location permission asked + * callback is used to interact with the site that requested the permission + */ + data class LocationPermissionRequest( + val origin: String, + val callback: GeolocationPermissions.Callback, + ) : PermissionRequest() { + + override fun getOrigin(): Uri { + return origin.toUri() + } + + override fun getResources(): Array { + return listOf(RESOURCE_LOCATION_PERMISSION).toTypedArray() + } + + override fun grant(p0: Array?) { + callback.invoke(origin, true, false) + } + + override fun deny() { + callback.invoke(origin, false, false) + } + + companion object { + const val RESOURCE_LOCATION_PERMISSION: String = "com.duckduckgo.permissions.resource.LOCATION_PERMISSION" + } + } } diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsDialogActivityLauncher.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsDialogActivityLauncher.kt index cc3471ce3c84..fa06340cd664 100644 --- a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsDialogActivityLauncher.kt +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsDialogActivityLauncher.kt @@ -27,8 +27,10 @@ import android.webkit.PermissionRequest import androidx.activity.result.ActivityResultCaller import androidx.annotation.StringRes import androidx.core.net.toUri +import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.common.ui.view.addClickableLink +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.ui.view.button.ButtonType.GHOST import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder import com.duckduckgo.common.ui.view.toPx import com.duckduckgo.common.utils.DispatcherProvider @@ -37,11 +39,12 @@ import com.duckduckgo.common.utils.extractDomain import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.site.permissions.api.SitePermissionsDialogLauncher import com.duckduckgo.site.permissions.api.SitePermissionsGrantedListener +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions -import com.duckduckgo.site.permissions.impl.databinding.ContentSiteDrmPermissionDialogBinding import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType +import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType.ALLOW_ALWAYS +import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType.DENY_ALWAYS import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionsEntity -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback import com.google.android.material.snackbar.Snackbar import com.squareup.anvil.annotations.ContributesBinding @@ -55,6 +58,8 @@ import timber.log.Timber class SitePermissionsDialogActivityLauncher @Inject constructor( private val systemPermissionsHelper: SystemPermissionsHelper, private val sitePermissionsRepository: SitePermissionsRepository, + private val faviconManager: FaviconManager, + private val pixel: Pixel, private val dispatcher: DispatcherProvider, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : SitePermissionsDialogLauncher { @@ -62,6 +67,7 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( private lateinit var sitePermissionRequest: PermissionRequest private lateinit var activity: Activity private lateinit var permissionRequested: SitePermissionsRequestedType + private var permissionPermanent: Boolean = false private lateinit var permissionsGrantedListener: SitePermissionsGrantedListener private lateinit var permissionsHandledByUser: List private lateinit var permissionsHandledAutomatically: List @@ -84,6 +90,7 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( request: PermissionRequest, permissionsGrantedListener: SitePermissionsGrantedListener, ) { + Timber.d("Permissions: permission askForSitePermission $permissionsRequested") sitePermissionRequest = request siteURL = url this.tabId = tabId @@ -96,37 +103,134 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( permissionsHandledByUser.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) && permissionsHandledByUser.contains( PermissionRequest.RESOURCE_AUDIO_CAPTURE, ) -> { - showSitePermissionsRationaleDialog(R.string.sitePermissionsMicAndCameraDialogTitle, url, this::askForMicAndCameraPermissions) + showSitePermissionsRationaleDialog( + R.string.sitePermissionsMicAndCameraDialogTitle, + R.string.sitePermissionsMicAndCameraDialogSubtitle, + url, + SitePermissionsPixelValues.CAMERA_AND_MICROPHONE, + { rememberChoice -> + askForMicAndCameraPermissions(rememberChoice) + }, + ) } + permissionsHandledByUser.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE) -> { - showSitePermissionsRationaleDialog(R.string.sitePermissionsMicDialogTitle, url, this::askForMicPermissions) + showSitePermissionsRationaleDialog( + R.string.sitePermissionsMicDialogTitle, + R.string.sitePermissionsMicDialogSubtitle, + url, + SitePermissionsPixelValues.MICROPHONE, + { rememberChoice -> + askForMicPermissions(rememberChoice) + }, + ) } + permissionsHandledByUser.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) -> { - showSitePermissionsRationaleDialog(R.string.sitePermissionsCameraDialogTitle, url, this::askForCameraPermissions) + showSitePermissionsRationaleDialog( + R.string.sitePermissionsCameraDialogTitle, + R.string.sitePermissionsCameraDialogSubtitle, + url, + SitePermissionsPixelValues.CAMERA, + { rememberChoice -> + askForCameraPermissions(rememberChoice) + }, + ) } + permissionsHandledByUser.contains(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) -> { showSiteDrmPermissionsDialog(activity, url) } + + permissionsHandledByUser.contains(LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION) -> { + showSiteLocationPermissionDialog(activity, request as LocationPermissionRequest, tabId) + } } } + private fun showSiteLocationPermissionDialog( + activity: Activity, + locationPermissionRequest: LocationPermissionRequest, + tabId: String, + ) { + sendDialogImpressionPixel(SitePermissionsPixelValues.LOCATION) + this.tabId = tabId + this.activity = activity + + val domain = locationPermissionRequest.origin.websiteFromGeoLocationsApiOrigin() + + val subtitle = if (domain == "duckduckgo.com") { + R.string.preciseLocationDDGDialogSubtitle + } else { + R.string.preciseLocationSiteDialogSubtitle + } + + TextAlertDialogBuilder(activity) + .setTitle( + String.format(activity.getString(R.string.sitePermissionsLocationDialogTitle), domain), + ) + .setMessage(subtitle) + .setPositiveButton(R.string.sitePermissionsDialogAllowButton, GHOST) + .setNegativeButton(R.string.sitePermissionsDialogDenyButton, GHOST) + .setCheckBoxText(R.string.sitePermissionsDialogRememberMeCheckBox) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + var rememberChoice = false + override fun onPositiveButtonClicked() { + if (rememberChoice) { + storeFavicon(locationPermissionRequest.origin) + } + sendPositiveDialogClickPixel(SitePermissionsPixelValues.LOCATION, rememberChoice) + askForLocationPermissions(rememberChoice) + } + + override fun onNegativeButtonClicked() { + if (rememberChoice) { + storeFavicon(locationPermissionRequest.origin) + } + sendNegativeDialogClickPixel(SitePermissionsPixelValues.LOCATION, rememberChoice) + denyPermissions(rememberChoice) + } + + override fun onCheckedChanged(checked: Boolean) { + rememberChoice = checked + } + }, + ) + .show() + } + private fun showSitePermissionsRationaleDialog( @StringRes titleRes: Int, + @StringRes messageRes: Int, url: String, - onPermissionAllowed: () -> Unit, + pixelType: String, + onPermissionAllowed: (Boolean) -> Unit, ) { + sendDialogImpressionPixel(pixelType) TextAlertDialogBuilder(activity) .setTitle(String.format(activity.getString(titleRes), url.websiteFromGeoLocationsApiOrigin())) - .setPositiveButton(R.string.sitePermissionsDialogAllowButton) - .setNegativeButton(R.string.sitePermissionsDialogDenyButton) + .setMessage(messageRes) + .setPositiveButton(R.string.sitePermissionsDialogAllowButton, GHOST) + .setNegativeButton(R.string.sitePermissionsDialogDenyButton, GHOST) + .setCheckBoxText(R.string.sitePermissionsDialogRememberMeCheckBox) .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + var rememberChoice = false override fun onPositiveButtonClicked() { - onPermissionAllowed() + onPermissionAllowed(rememberChoice) + sendPositiveDialogClickPixel(rememberChoice = rememberChoice, type = pixelType) } override fun onNegativeButtonClicked() { - denyPermissions() + denyPermissions(rememberChoice) + sendNegativeDialogClickPixel(rememberChoice = rememberChoice, type = pixelType) + } + + override fun onCheckedChanged(checked: Boolean) { + rememberChoice = checked + super.onCheckedChanged(checked) } }, ) @@ -137,6 +241,7 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( activity: Activity, url: String, ) { + sendDialogImpressionPixel(SitePermissionsPixelValues.DRM) val domain = url.extractDomain() ?: url // Check if user allowed or denied per session @@ -157,51 +262,58 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } // No session-based setting and no config --> proceed to show dialog - val binding = ContentSiteDrmPermissionDialogBinding.inflate(activity.layoutInflater) - val dialog = MaterialAlertDialogBuilder(activity) - .setView(binding.root) - .setOnCancelListener { - // Called when user clicks outside the dialog - deny to be safe + val title = url.websiteFromGeoLocationsApiOrigin() + TextAlertDialogBuilder(activity) + .setTitle( + String.format( + activity.getString(R.string.drmSiteDialogTitle), + title, + ), + ) + .setClickableMessage( + activity.getText(R.string.drmSiteDialogSubtitle), + DRM_LEARN_MORE_ANNOTATION, + ) { denyPermissions() + activity.startActivity(Intent(Intent.ACTION_VIEW, DRM_LEARN_MORE_URL)) } - .create() - - val title = url.websiteFromGeoLocationsApiOrigin() - binding.sitePermissionDialogTitle.text = activity.getString(R.string.drmSiteDialogTitle, title) - binding.sitePermissionDialogSubtitle.addClickableLink( - DRM_LEARN_MORE_ANNOTATION, - activity.getText(R.string.drmSiteDialogSubtitle), - ) { - denyPermissions() - dialog.dismiss() - activity.startActivity(Intent(Intent.ACTION_VIEW, DRM_LEARN_MORE_URL)) - } - - binding.siteAllowAlwaysDrmPermission.setOnClickListener { - grantPermissions() - onSiteDrmPermissionSave(domain, SitePermissionAskSettingType.ALLOW_ALWAYS) - dialog.dismiss() - } + .setPositiveButton(R.string.sitePermissionsDialogAllowButton, GHOST) + .setNegativeButton(R.string.sitePermissionsDialogDenyButton, GHOST) + .setCheckBoxText(R.string.sitePermissionsDialogRememberMeCheckBox) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { - binding.siteAllowOnceDrmPermission.setOnClickListener { - sitePermissionsRepository.saveDrmForSession(domain, true) - grantPermissions() - dialog.dismiss() - } + var rememberChoice = false - binding.siteDenyOnceDrmPermission.setOnClickListener { - sitePermissionsRepository.saveDrmForSession(domain, false) - denyPermissions() - dialog.dismiss() - } + override fun onPositiveButtonClicked() { + if (rememberChoice) { + grantPermissions() + onSiteDrmPermissionSave(domain, SitePermissionAskSettingType.ALLOW_ALWAYS) + storeFavicon(url) + } else { + sitePermissionsRepository.saveDrmForSession(domain, true) + grantPermissions() + } + sendPositiveDialogClickPixel(SitePermissionsPixelValues.DRM, rememberChoice) + } - binding.siteDenyAlwaysDrmPermission.setOnClickListener { - denyPermissions() - onSiteDrmPermissionSave(domain, SitePermissionAskSettingType.DENY_ALWAYS) - dialog.dismiss() - } + override fun onNegativeButtonClicked() { + denyPermissions(rememberChoice) + if (rememberChoice) { + onSiteDrmPermissionSave(domain, SitePermissionAskSettingType.DENY_ALWAYS) + storeFavicon(url) + } else { + sitePermissionsRepository.saveDrmForSession(domain, false) + } + sendNegativeDialogClickPixel(SitePermissionsPixelValues.DRM, rememberChoice) + } - dialog.show() + override fun onCheckedChanged(checked: Boolean) { + rememberChoice = checked + } + }, + ) + .show() } private fun onSiteDrmPermissionSave( @@ -218,15 +330,61 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } - private fun askForMicAndCameraPermissions() { + private fun sendDialogImpressionPixel(type: String) { + pixel.fire( + SitePermissionsPixelName.PERMISSION_DIALOG_IMPRESSION, + mapOf(SitePermissionsPixelParameters.PERMISSION_TYPE to type), + ) + } + + private fun sendNegativeDialogClickPixel( + type: String, + rememberChoice: Boolean, + ) { + val selection = if (rememberChoice) { + SitePermissionsPixelValues.DENY_ALWAYS + } else { + SitePermissionsPixelValues.DENY_ONCE + } + pixel.fire( + SitePermissionsPixelName.PERMISSION_DIALOG_CLICK, + mapOf( + SitePermissionsPixelParameters.PERMISSION_TYPE to type, + SitePermissionsPixelParameters.PERMISSION_SELECTION to selection, + ), + ) + } + + private fun sendPositiveDialogClickPixel( + type: String, + rememberChoice: Boolean, + ) { + val selection = if (rememberChoice) { + SitePermissionsPixelValues.ALLOW_ALWAYS + } else { + SitePermissionsPixelValues.ALLOW_ONCE + } + pixel.fire( + SitePermissionsPixelName.PERMISSION_DIALOG_CLICK, + mapOf( + SitePermissionsPixelParameters.PERMISSION_TYPE to type, + SitePermissionsPixelParameters.PERMISSION_SELECTION to selection, + ), + ) + } + + private fun askForMicAndCameraPermissions(rememberChoice: Boolean) { permissionRequested = SitePermissionsRequestedType.CAMERA_AND_AUDIO + permissionPermanent = rememberChoice when { systemPermissionsHelper.hasMicPermissionsGranted() && systemPermissionsHelper.hasCameraPermissionsGranted() -> { systemPermissionGranted() } + systemPermissionsHelper.hasMicPermissionsGranted() -> { systemPermissionsHelper.requestPermission(Manifest.permission.CAMERA) } + systemPermissionsHelper.hasCameraPermissionsGranted() -> { systemPermissionsHelper.requestMultiplePermissions( arrayOf( @@ -235,6 +393,7 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( ), ) } + else -> { systemPermissionsHelper.requestMultiplePermissions( arrayOf( @@ -247,8 +406,9 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } - private fun askForMicPermissions() { + private fun askForMicPermissions(rememberChoice: Boolean = false) { permissionRequested = SitePermissionsRequestedType.AUDIO + permissionPermanent = rememberChoice if (systemPermissionsHelper.hasMicPermissionsGranted()) { systemPermissionGranted() } else { @@ -256,8 +416,9 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } - private fun askForCameraPermissions() { + private fun askForCameraPermissions(rememberChoice: Boolean = false) { permissionRequested = SitePermissionsRequestedType.CAMERA + permissionPermanent = rememberChoice if (systemPermissionsHelper.hasCameraPermissionsGranted()) { systemPermissionGranted() } else { @@ -265,6 +426,18 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } + private fun askForLocationPermissions(rememberChoice: Boolean = false) { + permissionRequested = SitePermissionsRequestedType.LOCATION + permissionPermanent = rememberChoice + if (systemPermissionsHelper.hasLocationPermissionsGranted()) { + systemPermissionGranted() + } else { + systemPermissionsHelper.requestMultiplePermissions( + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), + ) + } + } + private fun onResultSystemPermissionRequest(granted: Boolean) { when (granted) { true -> systemPermissionGranted() @@ -293,7 +466,12 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( private fun systemPermissionGranted() { grantPermissions() permissionsHandledByUser.forEach { - sitePermissionsRepository.sitePermissionGranted(siteURL, tabId, it) + Timber.w("Permissions: sitePermission $it granted for $siteURL rememberChoice $permissionPermanent") + if (permissionPermanent) { + sitePermissionsRepository.sitePermissionPermanentlySaved(siteURL, it, ALLOW_ALWAYS) + } else { + sitePermissionsRepository.sitePermissionGranted(siteURL, tabId, it) + } } checkIfActionNeeded() } @@ -314,22 +492,37 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } - private fun showPermissionsDeniedSnackBar() { + private fun showPermissionsDeniedSnackBar(rememberChoice: Boolean = false) { val onPermissionAllowed: () -> Unit val message = when (permissionRequested) { SitePermissionsRequestedType.CAMERA -> { - onPermissionAllowed = this::askForCameraPermissions + onPermissionAllowed = { + askForCameraPermissions(rememberChoice) + } R.string.sitePermissionsCameraDeniedSnackBarMessage } + SitePermissionsRequestedType.AUDIO -> { - onPermissionAllowed = this::askForMicPermissions + onPermissionAllowed = { + askForMicPermissions(rememberChoice) + } R.string.sitePermissionsMicDeniedSnackBarMessage } + SitePermissionsRequestedType.CAMERA_AND_AUDIO -> { - onPermissionAllowed = this::askForMicAndCameraPermissions + onPermissionAllowed = { + askForMicAndCameraPermissions(rememberChoice) + } R.string.sitePermissionsCameraAndMicDeniedSnackBarMessage } + + SitePermissionsRequestedType.LOCATION -> { + onPermissionAllowed = { + askForLocationPermissions(rememberChoice) + } + R.string.sitePermissionsLocationDeniedSnackBarMessage + } } val snackbar = Snackbar.make(activity.window.decorView.rootView, message, Snackbar.LENGTH_LONG) @@ -357,12 +550,23 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } } - private fun denyPermissions() { + private fun denyPermissions(rememberChoice: Boolean = false) { + Timber.w("Permissions: sitePermission ${sitePermissionRequest.resources.asList()} denied for $siteURL rememberChoice $rememberChoice") try { if (permissionsHandledAutomatically.isNotEmpty()) { sitePermissionRequest.grant(permissionsHandledAutomatically.toTypedArray()) } else { sitePermissionRequest.deny() + + if (rememberChoice) { + sitePermissionRequest.resources.forEach { permission -> + sitePermissionsRepository.sitePermissionPermanentlySaved( + siteURL, + permission, + DENY_ALWAYS, + ) + } + } } } catch (e: IllegalStateException) { // IllegalStateException is thrown when grant() or deny() have been called already. @@ -371,16 +575,18 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( } private fun showSystemPermissionsDeniedDialog() { - denyPermissions() + denyPermissions(permissionPermanent) val titleRes = when (permissionRequested) { SitePermissionsRequestedType.CAMERA -> R.string.systemPermissionDialogCameraDeniedTitle SitePermissionsRequestedType.AUDIO -> R.string.systemPermissionDialogAudioDeniedTitle SitePermissionsRequestedType.CAMERA_AND_AUDIO -> R.string.systemPermissionDialogCameraAndAudioDeniedTitle + SitePermissionsRequestedType.LOCATION -> R.string.systemPermissionDialogLocationDeniedTitle } val contentRes = when (permissionRequested) { SitePermissionsRequestedType.CAMERA -> R.string.systemPermissionDialogCameraDeniedContent SitePermissionsRequestedType.AUDIO -> R.string.systemPermissionDialogAudioDeniedContent SitePermissionsRequestedType.CAMERA_AND_AUDIO -> R.string.systemPermissionDialogCameraAndAudioDeniedContent + SitePermissionsRequestedType.LOCATION -> R.string.systemPermissionDialogLocationDeniedContent } TextAlertDialogBuilder(activity) .setTitle(titleRes) @@ -401,25 +607,21 @@ class SitePermissionsDialogActivityLauncher @Inject constructor( .show() } + private fun storeFavicon(url: String) { + appCoroutineScope.launch { + faviconManager.persistCachedFavicon(tabId, url) + } + } + companion object { private const val DRM_LEARN_MORE_ANNOTATION = "drm_learn_more_link" val DRM_LEARN_MORE_URL = "https://duckduckgo.com/duckduckgo-help-pages/privacy/drm-permission/".toUri() } } -fun String.websiteFromGeoLocationsApiOrigin(): String { - val webPrefix = "www." - val uri = Uri.parse(this) - val host = uri.host ?: return this - - return host - .takeIf { it.startsWith(webPrefix, ignoreCase = true) } - ?.drop(webPrefix.length) - ?: host -} - enum class SitePermissionsRequestedType { CAMERA, AUDIO, CAMERA_AND_AUDIO, + LOCATION, } diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerImpl.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerImpl.kt index 549f78168131..0325294f82c8 100644 --- a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerImpl.kt +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerImpl.kt @@ -17,20 +17,25 @@ package com.duckduckgo.site.permissions.impl import android.content.pm.PackageManager +import android.location.LocationManager import android.webkit.PermissionRequest +import androidx.core.location.LocationManagerCompat import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.site.permissions.api.SitePermissionsManager +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissionQueryResponse import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.withContext +import timber.log.Timber // Cannot be a Singleton @ContributesBinding(AppScope::class) class SitePermissionsManagerImpl @Inject constructor( private val packageManager: PackageManager, + private val locationManager: LocationManager, private val sitePermissionsRepository: SitePermissionsRepository, private val dispatcherProvider: DispatcherProvider, ) : SitePermissionsManager { @@ -50,31 +55,42 @@ class SitePermissionsManagerImpl @Inject constructor( ): SitePermissions { val autoAccept = mutableListOf() val url = request.origin.toString() + val sitePermissionsAllowedToAsk = request.resources .filter { isPermissionSupported(it) && isHardwareSupported(it) } .filter { sitePermissionsRepository.isDomainAllowedToAsk(url, it) } .toTypedArray() + Timber.d("Permissions: sitePermissionsAllowedToAsk in $url ${sitePermissionsAllowedToAsk.asList()}") + val sitePermissionsGranted = getSitePermissionsGranted(url, tabId, sitePermissionsAllowedToAsk) if (sitePermissionsGranted.isNotEmpty()) { withContext(dispatcherProvider.main()) { + Timber.d("Permissions: site permission granted") autoAccept.addAll(sitePermissionsGranted) } } + + Timber.d("Permissions: sitePermissionsGranted for $url are ${sitePermissionsGranted.asList()}") + val userList = sitePermissionsAllowedToAsk.filter { !sitePermissionsGranted.contains(it) } if (userList.isEmpty() && sitePermissionsGranted.isEmpty()) { withContext(dispatcherProvider.main()) { + Timber.d("Permissions: site permission not granted, deny") request.deny() } } if (userList.isEmpty() && autoAccept.isNotEmpty()) { withContext(dispatcherProvider.main()) { + Timber.d("Permissions: site permission granted, auto accept") request.grant(autoAccept.toTypedArray()) autoAccept.clear() } } - return SitePermissions(autoAccept = autoAccept, userHandled = userList) + val sitePermissions = SitePermissions(autoAccept = autoAccept, userHandled = userList) + Timber.d("Permissions: site permissions $sitePermissions") + return sitePermissions } override suspend fun clearAllButFireproof(fireproofDomains: List) { @@ -102,14 +118,24 @@ class SitePermissionsManagerImpl @Inject constructor( return SitePermissionQueryResponse.Denied } + override suspend fun hasSitePermanentPermission( + url: String, + request: String, + ): Boolean { + return sitePermissionsRepository.isDomainGranted(url, "", LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION) + } + private fun isPermissionSupported(permission: String): Boolean = permission == PermissionRequest.RESOURCE_AUDIO_CAPTURE || permission == PermissionRequest.RESOURCE_VIDEO_CAPTURE || - permission == PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID + permission == PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID || permission == LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION private fun isHardwareSupported(permission: String): Boolean = when (permission) { PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { kotlin.runCatching { packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) }.getOrDefault(false) } + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION -> { + kotlin.runCatching { LocationManagerCompat.isLocationEnabled(locationManager) }.getOrDefault(false) + } else -> { true } diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsPixelName.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsPixelName.kt new file mode 100644 index 000000000000..21732dfe02ea --- /dev/null +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsPixelName.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.site.permissions.impl + +import com.duckduckgo.app.statistics.pixels.Pixel + +enum class SitePermissionsPixelName(override val pixelName: String) : Pixel.PixelName { + PERMISSION_DIALOG_IMPRESSION("m_site_permissions_dialog_impresssion"), + PERMISSION_DIALOG_CLICK("m_site_permissions_dialog_click"), +} + +object SitePermissionsPixelParameters { + const val PERMISSION_TYPE = "type" + const val PERMISSION_SELECTION = "selection" +} + +object SitePermissionsPixelValues { + const val LOCATION = "location" + const val CAMERA = "camera" + const val MICROPHONE = "microphone" + const val CAMERA_AND_MICROPHONE = "camera_and_microphone" + const val DRM = "drm" + const val ALLOW_ALWAYS = "allow_always" + const val ALLOW_ONCE = "allow_once" + const val DENY_ALWAYS = "deny_always" + const val DENY_ONCE = "deny_once" +} diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepository.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepository.kt index 88dcf983ab31..cc17ef82a74b 100644 --- a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepository.kt +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepository.kt @@ -21,6 +21,7 @@ import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.extractDomain import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.impl.drmblock.DrmBlock import com.duckduckgo.site.permissions.store.SitePermissionsPreferences import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType @@ -34,14 +35,17 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber interface SitePermissionsRepository { var askCameraEnabled: Boolean var askMicEnabled: Boolean var askDrmEnabled: Boolean + var askLocationEnabled: Boolean suspend fun isDomainAllowedToAsk(url: String, permission: String): Boolean suspend fun isDomainGranted(url: String, tabId: String, permission: String): Boolean fun sitePermissionGranted(url: String, tabId: String, permission: String) + fun sitePermissionPermanentlySaved(url: String, permission: String, settingType: SitePermissionAskSettingType) fun sitePermissionsWebsitesFlow(): Flow> fun sitePermissionsForAllWebsites(): List fun sitePermissionsAllowedFlow(): Flow> @@ -83,6 +87,12 @@ class SitePermissionsRepositoryImpl @Inject constructor( sitePermissionsPreferences.askDrmEnabled = value } + override var askLocationEnabled: Boolean + get() = sitePermissionsPreferences.askLocationEnabled + set(value) { + sitePermissionsPreferences.askLocationEnabled = value + } + private val drmSessions = mutableMapOf() override suspend fun isDomainAllowedToAsk(url: String, permission: String): Boolean { @@ -107,6 +117,12 @@ class SitePermissionsRepositoryImpl @Inject constructor( askDrmEnabled || sitePermissionsForDomain?.askDrmSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name isAskDrmDisabled && !isAskDrmSettingDenied } + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION -> { + val isLocationSettingDenied = sitePermissionsForDomain?.askLocationSetting == SitePermissionAskSettingType.DENY_ALWAYS.name + val isLocationDisabled = + askLocationEnabled || sitePermissionsForDomain?.askLocationSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name + isLocationDisabled && !isLocationSettingDenied + } else -> false } } @@ -115,19 +131,31 @@ class SitePermissionsRepositoryImpl @Inject constructor( val domain = url.extractDomain() ?: url val sitePermissionForDomain = sitePermissionsDao.getSitePermissionsByDomain(domain) val permissionAllowedEntity = sitePermissionsAllowedDao.getSitePermissionAllowed(domain, tabId, permission) + val permissionGrantedWithin24h = permissionAllowedEntity?.allowedWithin24h() == true + Timber.d("Permissions: permissionGrantedWithin24h $permissionGrantedWithin24h") return when (permission) { PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { val isCameraAlwaysAllowed = sitePermissionForDomain?.askCameraSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name + Timber.d("Permissions: isCameraAlwaysAllowed $isCameraAlwaysAllowed") permissionGrantedWithin24h || isCameraAlwaysAllowed } PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { val isMicAlwaysAllowed = sitePermissionForDomain?.askMicSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name + Timber.d("Permissions: isMicAlwaysAllowed $isMicAlwaysAllowed") + permissionGrantedWithin24h || isMicAlwaysAllowed } PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> { - sitePermissionForDomain?.askDrmSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name + val isDRMAlwaysAllowed = SitePermissionAskSettingType.ALLOW_ALWAYS.name + Timber.d("Permissions: isDRMAlwaysAllowed $isDRMAlwaysAllowed") + sitePermissionForDomain?.askDrmSetting == isDRMAlwaysAllowed + } + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION -> { + val isLocationAlwaysAllowed = sitePermissionForDomain?.askLocationSetting == SitePermissionAskSettingType.ALLOW_ALWAYS.name + Timber.d("Permissions: isLocationAlwaysAllowed $isLocationAlwaysAllowed") + permissionGrantedWithin24h || isLocationAlwaysAllowed } else -> false } @@ -190,9 +218,9 @@ class SitePermissionsRepositoryImpl @Inject constructor( sitePermissionsAllowedDao.deleteAll() } - override suspend fun getSitePermissionsForWebsite(url: String): SitePermissionsEntity? { + override suspend fun getSitePermissionsForWebsite(domain: String): SitePermissionsEntity? { return withContext(dispatcherProvider.io()) { - val domain = url.extractDomain() ?: url + Timber.d("Permissions: getSitePermissionsForWebsite for $domain") sitePermissionsDao.getSitePermissionsByDomain(domain) } } @@ -211,4 +239,32 @@ class SitePermissionsRepositoryImpl @Inject constructor( sitePermissionsDao.insert(sitePermissionsEntity) } } + + override fun sitePermissionPermanentlySaved(url: String, permission: String, settingType: SitePermissionAskSettingType) { + appCoroutineScope.launch(dispatcherProvider.io()) { + val domain = url.extractDomain() ?: url + + val permissionToUpdate = sitePermissionsDao.getSitePermissionsByDomain(domain) ?: SitePermissionsEntity(domain = domain) + + val permanentPermission = when (permission) { + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + permissionToUpdate.copy(askCameraSetting = settingType.name) + } + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + permissionToUpdate.copy(askMicSetting = settingType.name) + } + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> { + permissionToUpdate.copy(askDrmSetting = settingType.name) + } + LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION -> { + permissionToUpdate.copy(askLocationSetting = settingType.name) + } + else -> { + permissionToUpdate + } + } + + sitePermissionsDao.insert(permanentPermission) + } + } } diff --git a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SystemPermissionsHelper.kt b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SystemPermissionsHelper.kt index b7b4d089d148..a614b24d5589 100644 --- a/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SystemPermissionsHelper.kt +++ b/site-permissions/site-permissions-impl/src/main/java/com/duckduckgo/site/permissions/impl/SystemPermissionsHelper.kt @@ -33,6 +33,7 @@ import javax.inject.Inject interface SystemPermissionsHelper { fun hasMicPermissionsGranted(): Boolean fun hasCameraPermissionsGranted(): Boolean + fun hasLocationPermissionsGranted(): Boolean fun registerPermissionLaunchers( caller: ActivityResultCaller, onResultPermissionRequest: (Boolean) -> Unit, @@ -59,6 +60,11 @@ class SystemPermissionsHelperImpl @Inject constructor( override fun hasCameraPermissionsGranted(): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + override fun hasLocationPermissionsGranted(): Boolean { + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED + } + override fun registerPermissionLaunchers( caller: ActivityResultCaller, onResultPermissionRequest: (Boolean) -> Unit, diff --git a/site-permissions/site-permissions-impl/src/main/res/values/donottranslate.xml b/site-permissions/site-permissions-impl/src/main/res/values/donottranslate.xml index 29f008744034..0133047e20a0 100644 --- a/site-permissions/site-permissions-impl/src/main/res/values/donottranslate.xml +++ b/site-permissions/site-permissions-impl/src/main/res/values/donottranslate.xml @@ -16,11 +16,25 @@ - Grant %1$s permission to access DRM software on your device? + \"%1$s\" wants to open your DRM software Digital Rights Management software is required to play protected media content, but also provides access to device identifiers. Only grant permission to sites you trust with this data. You can manage permissions at any time in Settings. Learn More Always Only for This Session Deny Always Deny for This Session + + \"%1$s\" wants to access your location + Remember my choice + We only use your anonymous location to deliver better results, closer to you. You can manage the location access permissions you’ve granted to individual sites in Settings. + You can manage the location access permissions you’ve granted to individual sites in Settings. + Allow DuckDuckGo to ask for location access on this device + Sites can only use your location if you allow DuckDuckGo to ask for access. + Allow DuckDuckGo to ask for location access + + + You can manage the microphone and camera access permissions you’ve granted to individual sites in Settings. + You can manage the microphone access permissions you’ve granted to individual sites in Settings. + You can manage the camera access permissions you’ve granted to individual sites in Settings. + \ No newline at end of file diff --git a/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerTest.kt b/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerTest.kt index f218f124fd33..f66749b8308b 100644 --- a/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerTest.kt +++ b/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsManagerTest.kt @@ -17,6 +17,7 @@ package com.duckduckgo.site.permissions.impl import android.content.pm.PackageManager +import android.location.LocationManager import android.webkit.PermissionRequest import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -43,8 +44,14 @@ class SitePermissionsManagerTest { private val mockSitePermissionsRepository: SitePermissionsRepository = mock() private val mockPackageManager = mock() - - private val testee = SitePermissionsManagerImpl(mockPackageManager, mockSitePermissionsRepository, coroutineRule.testDispatcherProvider) + private val mockLocationManager = mock() + + private val testee = SitePermissionsManagerImpl( + mockPackageManager, + mockLocationManager, + mockSitePermissionsRepository, + coroutineRule.testDispatcherProvider, + ) private val url = "https://domain.com/whatever" private val tabId = "tabId" diff --git a/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepositoryTest.kt b/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepositoryTest.kt index 66e75f1eafa8..90f083431e45 100644 --- a/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepositoryTest.kt +++ b/site-permissions/site-permissions-impl/src/test/java/com/duckduckgo/site/permissions/impl/SitePermissionsRepositoryTest.kt @@ -19,9 +19,11 @@ package com.duckduckgo.site.permissions.impl import android.webkit.PermissionRequest import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.site.permissions.api.SitePermissionsManager.LocationPermissionRequest import com.duckduckgo.site.permissions.impl.drmblock.DrmBlock import com.duckduckgo.site.permissions.store.SitePermissionsPreferences import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType +import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionAskSettingType.ALLOW_ALWAYS import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionsDao import com.duckduckgo.site.permissions.store.sitepermissions.SitePermissionsEntity import com.duckduckgo.site.permissions.store.sitepermissionsallowed.SitePermissionAllowedEntity @@ -235,10 +237,10 @@ class SitePermissionsRepositoryTest { } @Test - fun whenGetSitePermissionsForWebsiteCalledThenGetSitePermissionsByDomain() = runTest { + fun whenGetSitePermissionsForWebsiteCalledThenGetSitePermissionsByTheSameUrl() = runTest { repository.getSitePermissionsForWebsite(url) - verify(mockSitePermissionsDao).getSitePermissionsByDomain(domain) + verify(mockSitePermissionsDao).getSitePermissionsByDomain(url) } @Test @@ -259,15 +261,37 @@ class SitePermissionsRepositoryTest { verify(mockSitePermissionsDao).insert(testEntity) } + @Test + fun whenPermissionAllowedPermanentlyForTheFirstTimeThenEntityInsertedInDb() = runTest { + val settingType = SitePermissionAskSettingType.ALLOW_ALWAYS + val testEntity = SitePermissionsEntity(domain, askLocationSetting = settingType.name) + whenever(mockSitePermissionsDao.getSitePermissionsByDomain(domain)).thenReturn(null) + repository.sitePermissionPermanentlySaved(testEntity.domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, ALLOW_ALWAYS) + + verify(mockSitePermissionsDao).insert(testEntity) + } + + @Test + fun whenPermissionAllowedPermanentlyThenEntityInsertedInDb() = runTest { + val settingType = SitePermissionAskSettingType.ALLOW_ALWAYS + val testEntity = SitePermissionsEntity(domain, askLocationSetting = settingType.name) + whenever(mockSitePermissionsDao.getSitePermissionsByDomain(domain)).thenReturn(testEntity) + repository.sitePermissionPermanentlySaved(testEntity.domain, LocationPermissionRequest.RESOURCE_LOCATION_PERMISSION, ALLOW_ALWAYS) + + verify(mockSitePermissionsDao).insert(testEntity) + } + private fun setInitialSettings( cameraEnabled: Boolean = true, micEnabled: Boolean = true, drmEnabled: Boolean = true, + locationEnabled: Boolean = true, sitePermissionEntity: SitePermissionsEntity? = null, ) = runTest { whenever(mockSitePermissionsPreferences.askCameraEnabled).thenReturn(cameraEnabled) whenever(mockSitePermissionsPreferences.askMicEnabled).thenReturn(micEnabled) whenever(mockSitePermissionsPreferences.askDrmEnabled).thenReturn(drmEnabled) + whenever(mockSitePermissionsPreferences.askLocationEnabled).thenReturn(locationEnabled) whenever(mockSitePermissionsDao.getSitePermissionsByDomain(domain)).thenReturn(sitePermissionEntity) } diff --git a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsDatabase.kt b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsDatabase.kt index 003832142eec..76075db32c23 100644 --- a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsDatabase.kt +++ b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsDatabase.kt @@ -29,7 +29,7 @@ import com.duckduckgo.site.permissions.store.sitepermissionsallowed.SitePermissi @Database( exportSchema = true, - version = 3, + version = 4, entities = [ SitePermissionsEntity::class, SitePermissionAllowedEntity::class, @@ -49,4 +49,10 @@ val MIGRATION_1_2 = object : Migration(1, 2) { } } -val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2) +val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE 'site_permissions' ADD COLUMN 'askLocationSetting' TEXT NOT NULL DEFAULT 'ASK_EVERY_TIME'") + } +} + +val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_3_4) diff --git a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsPreferences.kt b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsPreferences.kt index 0c5528535f05..fa254d016c8a 100644 --- a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsPreferences.kt +++ b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/SitePermissionsPreferences.kt @@ -26,6 +26,7 @@ interface SitePermissionsPreferences { var askCameraEnabled: Boolean var askMicEnabled: Boolean var askDrmEnabled: Boolean + var askLocationEnabled: Boolean } class SitePermissionsPreferencesImp @Inject constructor(private val context: Context) : SitePermissionsPreferences { @@ -44,11 +45,16 @@ class SitePermissionsPreferencesImp @Inject constructor(private val context: Con get() = preferences.getBoolean(KEY_ASK_DRM_ENABLED, true) set(enabled) = preferences.edit { putBoolean(KEY_ASK_DRM_ENABLED, enabled) } + override var askLocationEnabled: Boolean + get() = preferences.getBoolean(KEY_ASK_LOCATION_ENABLED, true) + set(enabled) = preferences.edit { putBoolean(KEY_ASK_LOCATION_ENABLED, enabled) } + companion object { const val FILENAME = "com.duckduckgo.site.permissions.settings" const val KEY_ASK_CAMERA_ENABLED = "ASK_CAMERA_ENABLED" const val KEY_ASK_MIC_ENABLED = "ASK_MIC_ENABLED" const val KEY_ASK_DRM_ENABLED = "ASK_DRM_ENABLED" + const val KEY_ASK_LOCATION_ENABLED = "ASK_LOCATION_ENABLED" } } diff --git a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/sitepermissions/SitePermissionsEntity.kt b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/sitepermissions/SitePermissionsEntity.kt index cfe23b544d7f..7e0705a24526 100644 --- a/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/sitepermissions/SitePermissionsEntity.kt +++ b/site-permissions/site-permissions-store/src/main/java/com/duckduckgo/site/permissions/store/sitepermissions/SitePermissionsEntity.kt @@ -26,6 +26,7 @@ data class SitePermissionsEntity( val askCameraSetting: String = ASK_EVERY_TIME.name, val askMicSetting: String = ASK_EVERY_TIME.name, val askDrmSetting: String = ASK_EVERY_TIME.name, + val askLocationSetting: String = ASK_EVERY_TIME.name, ) enum class SitePermissionAskSettingType {