diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt index 009ea0aa8976..782797548f8e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/animations/TrackersLottieAssetDelegate.kt @@ -92,7 +92,7 @@ internal class TrackersLottieAssetDelegate( } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = context.getColorFromAttr(com.duckduckgo.mobile.android.R.attr.daxColorContainer) + color = context.getColorFromAttr(com.duckduckgo.mobile.android.R.attr.omnibarRoundedFieldBackgroundColor) typeface = Typeface.SANS_SERIF } diff --git a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt index 60675677ca48..2d38bb6935fe 100644 --- a/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/globalprivacycontrol/ui/GlobalPrivacyControlActivity.kt @@ -77,6 +77,7 @@ class GlobalPrivacyControlActivity : DuckDuckGoActivity() { gpcSpannableString.getSpanFlags(it), ) removeSpan(it) + trim() } } binding.globalPrivacyControlDescription.apply { diff --git a/app/src/main/res/layout/activity_accessibility_settings.xml b/app/src/main/res/layout/activity_accessibility_settings.xml index b025536e3616..2c153370df97 100644 --- a/app/src/main/res/layout/activity_accessibility_settings.xml +++ b/app/src/main/res/layout/activity_accessibility_settings.xml @@ -14,114 +14,100 @@ ~ limitations under the License. --> - + + android:id="@+id/includeToolbar" + layout="@layout/include_default_toolbar" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:ignore="Overdraw"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="@dimen/keyline_4"> + android:id="@+id/appFontSizeToggle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:primaryText="@string/accessibilityTextSizeOverrideTitle" + app:secondaryText="@string/accessibilityTextSizeOverrideSubtitle" + app:showSwitch="true" /> + android:id="@+id/fontSizeSettingsGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + tools:ignore="Overdraw"> - + android:paddingEnd="@dimen/keyline_4"> - - - + android:layout_gravity="center" + android:minWidth="50dp" + app:typography="body1" /> + android:id="@+id/accessibilitySlider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingTop="@dimen/keyline_4" + android:paddingBottom="@dimen/keyline_4" + android:stepSize="10" + android:valueFrom="70" + android:valueTo="170" + app:labelBehavior="gone" + app:tickVisible="false" /> - + + android:layout_height="wrap_content" /> + android:id="@+id/forceZoomToggle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:primaryText="@string/accessibilityForceZoomTitle" + app:secondaryText="@string/accessibilityForceZoomSubtitle" + app:showSwitch="true" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_global_privacy_control.xml b/app/src/main/res/layout/activity_global_privacy_control.xml index 9aaf97b6b72f..b6fae6a2f7be 100644 --- a/app/src/main/res/layout/activity_global_privacy_control.xml +++ b/app/src/main/res/layout/activity_global_privacy_control.xml @@ -17,42 +17,29 @@ --> + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - - - + android:layout_height="wrap_content" + android:layout_margin="@dimen/keyline_4" + android:text="@string/globalPrivacyControlDescription" + app:textType="secondary" + app:typography="body2" /> + + diff --git a/app/src/main/res/layout/include_new_browser_tab.xml b/app/src/main/res/layout/include_new_browser_tab.xml index c7ef61f0bc95..9295e366cdd9 100644 --- a/app/src/main/res/layout/include_new_browser_tab.xml +++ b/app/src/main/res/layout/include_new_browser_tab.xml @@ -21,7 +21,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - android:clipChildren="false" tools:context="com.duckduckgo.app.browser.BrowserActivity" tools:showIn="@layout/fragment_browser_tab"> diff --git a/autoconsent/autoconsent-impl/build.gradle b/autoconsent/autoconsent-impl/build.gradle index 64968f7658b9..c33082926551 100644 --- a/autoconsent/autoconsent-impl/build.gradle +++ b/autoconsent/autoconsent-impl/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation AndroidX.appCompat implementation JakeWharton.timber implementation KotlinX.coroutines.core + implementation Google.android.material testImplementation (KotlinX.coroutines.test) { // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 diff --git a/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml b/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml index e02584d37ed5..db78f23954a0 100644 --- a/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml +++ b/autoconsent/autoconsent-impl/src/main/res/layout/activity_autoconsent_settings.xml @@ -15,34 +15,30 @@ --> + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + layout="@layout/include_default_toolbar" /> - + app:textType="secondary" + app:typography="body2" /> + android:layout_height="wrap_content" + app:primaryText="@string/autoconsentToggle" + app:primaryTextTruncated="false" + app:showSwitch="true" /> diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt new file mode 100644 index 000000000000..a5523539f10b --- /dev/null +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/ui/urlmatcher/AutofillUrlMatcher.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.autofill.ui.urlmatcher + +interface AutofillUrlMatcher { + fun extractUrlPartsForAutofill(originalUrl: String): ExtractedUrlParts + fun matchingForAutofill(visitedSite: ExtractedUrlParts, savedSite: ExtractedUrlParts): Boolean + + data class ExtractedUrlParts(val eTldPlus1: String?, val subdomain: String?) +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt index 4fdad217ffaa..0a2ccc1b248c 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/di/AutofillModule.kt @@ -24,6 +24,8 @@ import com.duckduckgo.autofill.store.RealAutofillPrefsStore import com.duckduckgo.autofill.store.RealInternalTestUserStore import com.duckduckgo.autofill.store.RealLastUpdatedTimeProvider import com.duckduckgo.autofill.store.SecureStoreBackedAutofillStore +import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher +import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.securestorage.api.SecureStorage import com.squareup.anvil.annotations.ContributesTo @@ -44,12 +46,17 @@ class AutofillModule { secureStorage: SecureStorage, context: Context, internalTestUserChecker: InternalTestUserChecker, + autofillUrlMatcher: AutofillUrlMatcher, ): AutofillStore { return SecureStoreBackedAutofillStore( - secureStorage, - internalTestUserChecker, - RealLastUpdatedTimeProvider(), - RealAutofillPrefsStore(context, internalTestUserChecker), + secureStorage = secureStorage, + internalTestUserChecker = internalTestUserChecker, + lastUpdatedTimeProvider = RealLastUpdatedTimeProvider(), + autofillPrefsStore = RealAutofillPrefsStore(context, internalTestUserChecker), + autofillUrlMatcher = autofillUrlMatcher, ) } + + @Provides + fun provideAutofillUrlMatcher(): AutofillUrlMatcher = AutofillDomainNameUrlMatcher() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt index f777e32e7b35..a91a89964405 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcher.kt @@ -16,20 +16,24 @@ package com.duckduckgo.autofill.ui.credential.management.suggestion -import com.duckduckgo.app.global.extractSchemeAndDomain import com.duckduckgo.autofill.domain.app.LoginCredentials +import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher import javax.inject.Inject -class SuggestionMatcher @Inject constructor() { +class SuggestionMatcher @Inject constructor(private val autofillUrlMatcher: AutofillUrlMatcher) { fun getSuggestions( currentUrl: String?, credentials: List, ): List { if (currentUrl == null) return emptyList() + val currentSite = autofillUrlMatcher.extractUrlPartsForAutofill(currentUrl) + if (currentSite.eTldPlus1 == null) return emptyList() return credentials.filter { - it.domain?.extractSchemeAndDomain() == currentUrl.extractSchemeAndDomain() + val storedDomain = it.domain ?: return@filter false + val savedSite = autofillUrlMatcher.extractUrlPartsForAutofill(storedDomain) + return@filter autofillUrlMatcher.matchingForAutofill(currentSite, savedSite) } } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt index 788fd43bde6d..6f5d0d84e01b 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/ui/credential/management/suggestion/SuggestionMatcherTest.kt @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.ui.credential.management.suggestion import com.duckduckgo.autofill.domain.app.LoginCredentials +import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith @@ -25,7 +26,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class SuggestionMatcherTest { - private val testee = SuggestionMatcher() + private val testee = SuggestionMatcher(AutofillDomainNameUrlMatcher()) @Test fun whenUrlIsNullThenNoSuggestions() { @@ -69,6 +70,13 @@ class SuggestionMatcherTest { assertEquals(2, suggestions.size) } + @Test + fun whenSubdomainIncludedInSavedSiteAndVisitingRootSiteThenSuggestionOffered() { + val creds = listOf(creds("https://duckduckgo.com")) + val suggestions = testee.getSuggestions("https://test.duckduckgo.com", creds) + assertEquals(1, suggestions.size) + } + private fun creds(domain: String): LoginCredentials { return LoginCredentials(id = 0, domain = domain, username = "username", password = "password") } diff --git a/autofill/autofill-store/build.gradle b/autofill/autofill-store/build.gradle index bd38a0434874..b61bbe0c903d 100644 --- a/autofill/autofill-store/build.gradle +++ b/autofill/autofill-store/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation KotlinX.coroutines.core implementation Google.android.material implementation project(path: ':secure-storage-api') + implementation Square.okHttp3.okHttp implementation Google.dagger implementation JakeWharton.timber diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt index b35ac6af503c..df389f13f2a5 100644 --- a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStore.kt @@ -26,6 +26,7 @@ import com.duckduckgo.autofill.InternalTestUserChecker import com.duckduckgo.autofill.domain.app.LoginCredentials import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.NoMatch +import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher import com.duckduckgo.securestorage.api.SecureStorage import com.duckduckgo.securestorage.api.WebsiteLoginDetails import com.duckduckgo.securestorage.api.WebsiteLoginDetailsWithCredentials @@ -41,6 +42,7 @@ class SecureStoreBackedAutofillStore( private val lastUpdatedTimeProvider: LastUpdatedTimeProvider, private val autofillPrefsStore: AutofillPrefsStore, private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(), + private val autofillUrlMatcher: AutofillUrlMatcher, ) : AutofillStore { override val autofillAvailable: Boolean @@ -79,13 +81,21 @@ class SecureStoreBackedAutofillStore( override suspend fun getCredentials(rawUrl: String): List { return withContext(dispatcherProvider.io()) { return@withContext if (autofillEnabled && autofillAvailable) { - Timber.i("Querying secure store for stored credentials. rawUrl: %s, extractedDomain:%s", rawUrl, rawUrl.extractSchemeAndDomain()) - val url = rawUrl.extractSchemeAndDomain() ?: return@withContext emptyList() + Timber.i("Querying secure store for stored credentials. rawUrl: %s", rawUrl) - val storedCredentials = secureStorage.websiteLoginDetailsWithCredentialsForDomain(url).firstOrNull() ?: emptyList() - Timber.v("Found %d credentials for %s", storedCredentials.size, url) + val visitedSite = autofillUrlMatcher.extractUrlPartsForAutofill(rawUrl) + if (visitedSite.eTldPlus1 == null) return@withContext emptyList() - storedCredentials.map { it.toLoginCredentials() } + // first part of domain matching happens at the DB level + val storedCredentials = + secureStorage.websiteLoginDetailsWithCredentialsForDomain(visitedSite.eTldPlus1!!).firstOrNull() ?: emptyList() + + // second part of domain matching requires filtering at code level + storedCredentials.filter { + val storedDomain = it.details.domain ?: return@filter false + val savedSite = autofillUrlMatcher.extractUrlPartsForAutofill(storedDomain) + return@filter autofillUrlMatcher.matchingForAutofill(visitedSite, savedSite) + }.map { it.toLoginCredentials() } } else { emptyList() } diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt new file mode 100644 index 000000000000..6e0ff4d8c458 --- /dev/null +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcher.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022 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.autofill.store.urlmatcher + +import com.duckduckgo.app.global.extractDomain +import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher +import javax.inject.Inject +import okhttp3.HttpUrl.Companion.toHttpUrl +import timber.log.Timber + +class AutofillDomainNameUrlMatcher @Inject constructor() : AutofillUrlMatcher { + + override fun extractUrlPartsForAutofill(originalUrl: String): AutofillUrlMatcher.ExtractedUrlParts { + val normalizedUrl = originalUrl.normalizeScheme() + return try { + val eTldPlus1 = normalizedUrl.toHttpUrl().topPrivateDomain() + val domain = originalUrl.extractDomain() + val subdomain = determineSubdomain(domain, eTldPlus1) + AutofillUrlMatcher.ExtractedUrlParts(eTldPlus1, subdomain) + } catch (e: IllegalArgumentException) { + Timber.w("Unable to parse e-tld+1 from $originalUrl") + AutofillUrlMatcher.ExtractedUrlParts(null, null) + } + } + + private fun determineSubdomain( + domain: String?, + eTldPlus1: String?, + ): String? { + if (eTldPlus1 == null) return null + + val subdomain = domain?.replace(eTldPlus1 ?: "", "", ignoreCase = true)?.removeSuffix(".") + if (subdomain?.isBlank() == true) return null + + return subdomain + } + + override fun matchingForAutofill( + visitedSite: AutofillUrlMatcher.ExtractedUrlParts, + savedSite: AutofillUrlMatcher.ExtractedUrlParts, + ): Boolean { + // e-tld+1 must match + if (!identicalEffectiveTldPlusOne(visitedSite, savedSite)) return false + + // any of these rules can match + return identicalSubdomains(visitedSite, savedSite) || + specialHandlingForWwwSubdomainOnSavedSite(visitedSite, savedSite) || + savedSiteHasNoSubdomain(savedSite) + } + + private fun identicalEffectiveTldPlusOne( + visitedSite: AutofillUrlMatcher.ExtractedUrlParts, + savedSite: AutofillUrlMatcher.ExtractedUrlParts, + ): Boolean { + return visitedSite.eTldPlus1.equals(savedSite.eTldPlus1, ignoreCase = true) + } + + private fun identicalSubdomains( + visitedSite: AutofillUrlMatcher.ExtractedUrlParts, + savedSite: AutofillUrlMatcher.ExtractedUrlParts, + ): Boolean { + return visitedSite.subdomain.equals(savedSite.subdomain, ignoreCase = true) + } + + private fun specialHandlingForWwwSubdomainOnSavedSite( + visitedSite: AutofillUrlMatcher.ExtractedUrlParts, + savedSite: AutofillUrlMatcher.ExtractedUrlParts, + ): Boolean { + return (visitedSite.subdomain == null && savedSite.subdomain.equals(WWW, ignoreCase = true)) + } + + private fun savedSiteHasNoSubdomain(savedSite: AutofillUrlMatcher.ExtractedUrlParts): Boolean { + return savedSite.subdomain == null + } + + private fun String.normalizeScheme(): String { + if (!startsWith("https://") && !startsWith("http://")) { + return "https://$this" + } + return this + } + + companion object { + private const val WWW = "www" + } +} diff --git a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt index fcd9c0bad22e..1c0a5a9220e3 100644 --- a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt +++ b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/SecureStoreBackedAutofillStoreTest.kt @@ -27,6 +27,8 @@ import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.NoM import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UrlOnlyMatch import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UsernameMatch import com.duckduckgo.autofill.store.AutofillStore.ContainsCredentialsResult.UsernameMissing +import com.duckduckgo.autofill.store.urlmatcher.AutofillDomainNameUrlMatcher +import com.duckduckgo.autofill.ui.urlmatcher.AutofillUrlMatcher import com.duckduckgo.securestorage.api.SecureStorage import com.duckduckgo.securestorage.api.WebsiteLoginDetails import com.duckduckgo.securestorage.api.WebsiteLoginDetailsWithCredentials @@ -65,6 +67,8 @@ class SecureStoreBackedAutofillStoreTest { private lateinit var internalTestUserChecker: FakeInternalTestUserChecker private lateinit var secureStore: FakeSecureStore + private val autofillUrlMatcher: AutofillUrlMatcher = AutofillDomainNameUrlMatcher() + @Before fun setUp() { MockitoAnnotations.openMocks(this) @@ -349,6 +353,75 @@ class SecureStoreBackedAutofillStoreTest { assertNull(testee.getCredentialsWithId(1)) } + @Test + fun whenExactUrlMatchesThenCredentialsReturned() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://example.com" + val visitedSite = "https://example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(1, results.size) + assertEquals(1L, results[0].id) + } + + @Test + fun whenExactSubdomainMatchesThenCredentialsReturned() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://subdomain.example.com" + val visitedSite = "https://subdomain.example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(1, results.size) + assertEquals(1L, results[0].id) + } + + @Test + fun whenExactWwwSubdomainMatchesThenCredentialsReturned() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://www.example.com" + val visitedSite = "https://www.example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(1, results.size) + assertEquals(1L, results[0].id) + } + + @Test + fun whenSavedCredentialHasSubdomainAndVisitedPageDoesNotThenCredentialNotMatched() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://test.example.com" + val visitedSite = "https://example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(0, results.size) + } + + @Test + fun whenSavedCredentialHasNoSubdomainAndVisitedPageDoesThenCredentialNotMatched() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://example.com" + val visitedSite = "https://test.example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(1, results.size) + } + + @Test + fun whenSavedCredentialHasDifferentSubdomainToVisitedPageSubdomainThenCredentialNotMatched() = runTest { + setupTesteeWithAutofillAvailable() + val savedUrl = "https://test.example.com" + val visitedSite = "https://different.example.com" + storeCredentials(1, savedUrl, "username1", "password123") + + val results = testee.getCredentials(visitedSite) + assertEquals(0, results.size) + } + private fun List.assertHasNoLoginCredentials( url: String, username: String, @@ -376,7 +449,13 @@ class SecureStoreBackedAutofillStoreTest { private fun setupTesteeWithAutofillAvailable() { internalTestUserChecker = FakeInternalTestUserChecker(true) secureStore = FakeSecureStore(true) - testee = SecureStoreBackedAutofillStore(secureStore, internalTestUserChecker, lastUpdatedTimeProvider, autofillPrefsStore) + testee = SecureStoreBackedAutofillStore( + secureStorage = secureStore, + internalTestUserChecker = internalTestUserChecker, + lastUpdatedTimeProvider = lastUpdatedTimeProvider, + autofillPrefsStore = autofillPrefsStore, + autofillUrlMatcher = autofillUrlMatcher, + ) } private fun setupTestee( @@ -386,11 +465,12 @@ class SecureStoreBackedAutofillStoreTest { internalTestUserChecker = FakeInternalTestUserChecker(isInternalUser) secureStore = FakeSecureStore(canAccessSecureStorage) testee = SecureStoreBackedAutofillStore( - secureStore, - internalTestUserChecker, - lastUpdatedTimeProvider, - autofillPrefsStore, + secureStorage = secureStore, + internalTestUserChecker = internalTestUserChecker, + lastUpdatedTimeProvider = lastUpdatedTimeProvider, + autofillPrefsStore = autofillPrefsStore, dispatcherProvider = coroutineTestRule.testDispatcherProvider, + autofillUrlMatcher = autofillUrlMatcher, ) } @@ -442,7 +522,7 @@ class SecureStoreBackedAutofillStoreTest { return flow { emit( credentials.filter { - it.details.domain == domain + it.details.domain?.contains(domain) == true }.map { it.details }, @@ -464,7 +544,7 @@ class SecureStoreBackedAutofillStoreTest { return flow { emit( credentials.filter { - it.details.domain == domain + it.details.domain?.contains(domain) == true }, ) } diff --git a/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt new file mode 100644 index 000000000000..e6fcc7df8fc6 --- /dev/null +++ b/autofill/autofill-store/src/test/java/com/duckduckgo/autofill/store/urlmatcher/AutofillDomainNameUrlMatcherTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2022 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.autofill.store.urlmatcher + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AutofillDomainNameUrlMatcherTest { + + private val testee = AutofillDomainNameUrlMatcher() + + @Test + fun whenBasicDomainThenSameReturnedForEtldPlus1() { + val inputUrl = "duckduckgo.com" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals(inputUrl, result.eTldPlus1) + } + + @Test + fun whenUrlIsInvalidMissingTldThenEtldPlusOneIsNull() { + val inputUrl = "duckduckgo" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertNull(result.eTldPlus1) + } + + @Test + fun whenUrlIsInvalidMissingTldThenSubdomainIsNull() { + val inputUrl = "duckduckgo" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertNull(result.subdomain) + } + + @Test + fun whenContainsHttpSchemeThenEtldPlus1Returned() { + val inputUrl = "http://duckduckgo.com" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.com", result.eTldPlus1) + } + + @Test + fun whenContainsHttpsSchemeThenEtldPlus1Returned() { + val inputUrl = "https://duckduckgo.com" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.com", result.eTldPlus1) + } + + @Test + fun whenContainsWwwSubdomainThenEtldPlus1Returned() { + val inputUrl = "www.duckduckgo.com" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.com", result.eTldPlus1) + } + + @Test + fun whenContainsAnotherSubdomainThenEtldPlus1Returned() { + val inputUrl = "foo.duckduckgo.com" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.com", result.eTldPlus1) + } + + @Test + fun whenTwoPartTldWithNoSubdomainThenEtldPlus1Returned() { + val inputUrl = "duckduckgo.co.uk" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.co.uk", result.eTldPlus1) + } + + @Test + fun whenTwoPartTldWithSubdomainThenEtldPlus1Returned() { + val inputUrl = "www.duckduckgo.co.uk" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("duckduckgo.co.uk", result.eTldPlus1) + } + + @Test + fun whenSubdomainIsWwwThenCorrectlyIdentifiedAsSubdomain() { + val inputUrl = "www.duckduckgo.co.uk" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("www", result.subdomain) + } + + @Test + fun whenSubdomainIsPresentButNotWwwThenCorrectlyIdentifiedAsSubdomain() { + val inputUrl = "test.duckduckgo.co.uk" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("test", result.subdomain) + } + + @Test + fun whenSubdomainHasTwoLevelsThenCorrectlyIdentifiedAsSubdomain() { + val inputUrl = "foo.bar.duckduckgo.co.uk" + val result = testee.extractUrlPartsForAutofill(inputUrl) + assertEquals("foo.bar", result.subdomain) + } + + @Test + fun whenUrlsAreIdenticalThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("https://example.com") + val visitedSite = testee.extractUrlPartsForAutofill("https://example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenUrlsAreIdenticalExceptForUppercaseVisitedSiteThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("https://example.com") + val visitedSite = testee.extractUrlPartsForAutofill("https://EXAMPLE.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenUrlsAreIdenticalExceptForUppercaseSavedSiteThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("https://EXAMPLE.com") + val visitedSite = testee.extractUrlPartsForAutofill("https://example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenBothUrlsContainSameSubdomainThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("test.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("test.example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenBothUrlsContainWwwSubdomainThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("www.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("www.example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteContainsSubdomainAndVisitedSiteDoesNotThenNoMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("foo.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("example.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("example.com") + val visitedSite = testee.extractUrlPartsForAutofill("foo.example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenUrlsHaveDifferentSubdomainsThenNoMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("bar.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("foo.example.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteContainsWwwSubdomainAndVisitedSiteDoesNotThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("www.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteContainsUppercaseWwwSubdomainAndVisitedSiteDoesNotThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("WWW.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesContainWwwSubdomainThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("example.com") + val visitedSite = testee.extractUrlPartsForAutofill("www.example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteDoesNotContainSubdomainAndVisitedSiteDoesContainUppercaseWwwSubdomainThenMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("example.com") + val visitedSite = testee.extractUrlPartsForAutofill("WWW.example.com") + assertTrue(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteContainNestedSubdomainsAndVisitedSiteContainsMatchingRootSubdomainThenNotMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("a.b.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("b.example.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteContainSubdomainAndVisitedSiteContainsNestedSubdomainsThenNotMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("b.example.com") + val visitedSite = testee.extractUrlPartsForAutofill("a.b.example.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedSiteHasNoSubdomainAndVisitedMaliciousSitePartiallyContainSavedSiteThenNoMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("example.com") + val visitedSite = testee.extractUrlPartsForAutofill("example.com.malicious.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } + + @Test + fun whenSavedMaliciousSitePartiallyContainsVisitedSiteThenNoMatchingForAutofill() { + val savedSite = testee.extractUrlPartsForAutofill("example.com.malicious.com") + val visitedSite = testee.extractUrlPartsForAutofill("example.com") + assertFalse(testee.matchingForAutofill(visitedSite, savedSite)) + } +} diff --git a/common-ui/src/main/res/drawable/background_text_view_container.xml b/common-ui/src/main/res/drawable/background_text_view_container.xml new file mode 100644 index 000000000000..accb8406b0c9 --- /dev/null +++ b/common-ui/src/main/res/drawable/background_text_view_container.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/common-ui/src/main/res/drawable/popup_menu_bg.xml b/common-ui/src/main/res/drawable/popup_menu_bg.xml index deb447fae7ff..3b8d30ba237a 100644 --- a/common-ui/src/main/res/drawable/popup_menu_bg.xml +++ b/common-ui/src/main/res/drawable/popup_menu_bg.xml @@ -15,6 +15,6 @@ --> - - + + \ No newline at end of file diff --git a/common-ui/src/main/res/layout/view_dax_dialog.xml b/common-ui/src/main/res/layout/view_dax_dialog.xml index a73bad53b379..78aad676f524 100644 --- a/common-ui/src/main/res/layout/view_dax_dialog.xml +++ b/common-ui/src/main/res/layout/view_dax_dialog.xml @@ -25,8 +25,7 @@ + android:layout_height="match_parent"> + diff --git a/common-ui/src/main/res/values/design-system-theming.xml b/common-ui/src/main/res/values/design-system-theming.xml index 13194f11254e..8f2a85a61f23 100644 --- a/common-ui/src/main/res/values/design-system-theming.xml +++ b/common-ui/src/main/res/values/design-system-theming.xml @@ -137,23 +137,19 @@ @color/black60 @color/white - ?attr/daxColorSurface - ?attr/daxColorSurface - - - ?attr/daxColorSurface - ?attr/daxColorPrimaryIcon - ?attr/daxColorPrimaryText - ?attr/daxColorSecondaryText diff --git a/common-ui/src/main/res/values/widgets.xml b/common-ui/src/main/res/values/widgets.xml index 393fa4b5152b..15bf52a0458b 100644 --- a/common-ui/src/main/res/values/widgets.xml +++ b/common-ui/src/main/res/values/widgets.xml @@ -24,12 +24,12 @@ diff --git a/secure-storage/secure-storage-store/build.gradle b/secure-storage/secure-storage-store/build.gradle index 1e9e1949854c..7f6012ea10db 100644 --- a/secure-storage/secure-storage-store/build.gradle +++ b/secure-storage/secure-storage-store/build.gradle @@ -42,6 +42,9 @@ dependencies { kapt AndroidX.room.compiler testImplementation Testing.junit4 + testImplementation project(path: ':common-test') + testImplementation Testing.robolectric + testImplementation AndroidX.archCore.testing testImplementation "org.mockito.kotlin:mockito-kotlin:_" testImplementation (KotlinX.coroutines.test) { // https://github.com/Kotlin/kotlinx.coroutines/issues/2023 diff --git a/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt b/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt index a3ac9a3770db..08d0fb22c792 100644 --- a/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt +++ b/secure-storage/secure-storage-store/src/main/java/com/duckduckgo/securestorage/store/db/WebsiteLoginCredentialsDao.kt @@ -36,7 +36,7 @@ interface WebsiteLoginCredentialsDao { @Query("select * from website_login_credentials") fun websiteLoginCredentials(): Flow> - @Query("select * from website_login_credentials where domain = :domain") + @Query("select * from website_login_credentials where domain like '%' || :domain || '%'") fun websiteLoginCredentialsByDomain(domain: String): Flow> @Query("select * from website_login_credentials where id = :id") diff --git a/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt b/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt index 8ccf0fbb9615..a4a7fb2f7cf9 100644 --- a/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt +++ b/secure-storage/secure-storage-store/src/test/java/com/duckduckgo/securestorage/store/RealSecureStorageRepositoryTest.kt @@ -16,97 +16,153 @@ package com.duckduckgo.securestorage.store +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.securestorage.store.db.SecureStorageDatabase import com.duckduckgo.securestorage.store.db.WebsiteLoginCredentialsDao import com.duckduckgo.securestorage.store.db.WebsiteLoginCredentialsEntity import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test -import org.mockito.Mock +import org.junit.runner.RunWith import org.mockito.MockitoAnnotations -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment @ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) class RealSecureStorageRepositoryTest { - @Mock + + @get:Rule + @Suppress("unused") + val coroutineRule = CoroutineTestRule() + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var db: SecureStorageDatabase + private lateinit var dao: WebsiteLoginCredentialsDao private lateinit var testee: RealSecureStorageRepository - private val testEntity = WebsiteLoginCredentialsEntity( - id = 1, - domain = "test.com", - username = "test", - password = "pass123", - passwordIv = "iv", - notes = "my notes", - notesIv = "notesIv", - domainTitle = "test", - lastUpdatedInMillis = 0L, - ) @Before fun setUp() { MockitoAnnotations.openMocks(this) + db = Room.inMemoryDatabaseBuilder(RuntimeEnvironment.getApplication(), SecureStorageDatabase::class.java) + .allowMainThreadQueries() + .build() + dao = db.websiteLoginCredentialsDao() testee = RealSecureStorageRepository(dao) } - @Test - fun whenAddWebsiteLoginCredentialThenCallInsertToDao() { - runTest { - testee.addWebsiteLoginCredential(testEntity) - } - - verify(dao).insert(testEntity) + @After + fun after() { + db.close() } @Test - fun whenGetWebsiteLoginCredentialsWithDomainThenCallGetWithDomainFromDao() = runTest { - whenever(dao.websiteLoginCredentialsByDomain("test")).thenReturn( - MutableStateFlow(listOf(testEntity)), - ) + fun whenRetrievingLoginCredentialByIdThenNullReturnedIfNotExists() = runTest { + assertNull(dao.getWebsiteLoginCredentialsById(entity().id)) + } - val result: List = - testee.websiteLoginCredentialsForDomain("test").first() + @Test + fun whenRetrievingLoginCredentialByIdThenReturnedIfExists() = runTest { + val testEntity = entity() + dao.insert(entity()) + val result = testee.getWebsiteLoginCredentialsForId(testEntity.id) + assertEquals(testEntity, result) + } + @Test + fun whenRetrievingLoginCredentialByDomainThenReturnedIfDirectMatch() = runTest { + val testEntity = entity() + dao.insert(testEntity) + val result: List = testee.websiteLoginCredentialsForDomain("test.com").first() assertEquals(testEntity, result[0]) } @Test - fun whenGetAllWebsiteLoginCredentialsThenCallGetAllFromDao() = runTest { - whenever(dao.websiteLoginCredentials()).thenReturn( - MutableStateFlow(listOf(testEntity)), - ) + fun whenRetrievingLoginCredentialByDomainThenEmptyListReturnedIfNoMatches() = runTest { + val testEntity = entity() + dao.insert(testEntity) + val result: List = testee.websiteLoginCredentialsForDomain("no-matches.com").first() + assertTrue(result.isEmpty()) + } - val result: List = - testee.websiteLoginCredentials().first() + @Test + fun whenGetAllWebsiteLoginCredentialsWithSitesThenEmptyListReturned() = runTest { + val result: List = testee.websiteLoginCredentials().first() + assertTrue(result.isEmpty()) + } + @Test + fun whenGetAllWebsiteLoginCredentialsWithASingleSiteThenThatOneIsReturned() = runTest { + val testEntity = entity() + dao.insert(testEntity) + val result: List = testee.websiteLoginCredentials().first() assertEquals(listOf(testEntity), result) } @Test - fun whenGetWebsiteLoginCredentialsWithIDThenCallGetWithIDFromDao() = runTest { - whenever(dao.getWebsiteLoginCredentialsById(1)).thenReturn(testEntity) - - val result = - testee.getWebsiteLoginCredentialsForId(1) - - assertEquals(testEntity, result) + fun whenGetAllWebsiteLoginCredentialsWithMultipleSitesThenThatAllReturned() = runTest { + val testEntity = entity() + val anotherEntity = entity(id = testEntity.id + 1) + dao.insert(testEntity) + dao.insert(anotherEntity) + val result: List = testee.websiteLoginCredentials().first() + assertEquals(listOf(testEntity, anotherEntity), result) } @Test fun whenUpdateWebsiteLoginCredentialsThenCallUpdateToDao() = runTest { - testee.updateWebsiteLoginCredentials(testEntity) - - verify(dao).update(testEntity) + val testEntity = entity() + dao.insert(testEntity) + testee.updateWebsiteLoginCredentials(testEntity.copy(username = "newUsername")) + val updated = testee.getWebsiteLoginCredentialsForId(testEntity.id) + assertNotNull(updated) + assertEquals("newUsername", updated!!.username) } @Test - fun whenDeleteWebsiteLoginCredentialsThenCallDeleteToDao() = runTest { + fun whenDeleteWebsiteLoginCredentialsThenEntityRemoved() = runTest { + val testEntity = entity() + dao.insert(testEntity) testee.deleteWebsiteLoginCredentials(1) + val nowDeleted = dao.getWebsiteLoginCredentialsById(testEntity.id) + assertNull(nowDeleted) + } - verify(dao).delete(1) + private fun entity( + id: Long = 1, + domain: String = "test.com", + username: String = "test", + password: String = "pass123", + passwordIv: String = "iv", + notes: String = "my notes", + notesIv: String = "notesIv", + domainTitle: String = "test", + lastUpdatedInMillis: Long = 0L, + ): WebsiteLoginCredentialsEntity { + return WebsiteLoginCredentialsEntity( + id = id, + domain = domain, + username = username, + password = password, + passwordIv = passwordIv, + notes = notes, + notesIv = notesIv, + domainTitle = domainTitle, + lastUpdatedInMillis = lastUpdatedInMillis, + ) } } diff --git a/versions.properties b/versions.properties index 9ca4d8b29fdf..9f1b04227c57 100644 --- a/versions.properties +++ b/versions.properties @@ -67,7 +67,7 @@ version.com.github.bumptech.glide..okhttp3-integration=4.13.2 version.com.nhaarman.mockitokotlin2..mockito-kotlin=2.2.0 -version.google.android.material=1.6.1 +version.google.android.material=1.7.0 version.google.dagger=2.44.2