From b52b43e7b9673f7d4d5025a791692d58f28487a4 Mon Sep 17 00:00:00 2001 From: Alexandru2909 Date: Tue, 3 May 2022 17:27:30 +0300 Subject: [PATCH] For #12060 - Add support for select address prompt request --- .../browser/engine/gecko/ext/Address.kt | 46 ++++++++ .../gecko/prompt/GeckoPromptDelegate.kt | 36 ++++++ .../gecko/prompt/GeckoPromptDelegateTest.kt | 103 ++++++++++++++++++ .../concept/engine/prompt/PromptRequest.kt | 16 +++ .../engine/prompt/PromptRequestTest.kt | 49 +++++++++ .../storage/CreditCardsAddressesStorage.kt | 10 +- .../feature/prompts/PromptFeature.kt | 2 + ...AutofillCreditCardsAddressesStorageTest.kt | 12 +- docs/changelog.md | 3 + 9 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt new file mode 100644 index 00000000000..f2317cac85a --- /dev/null +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.engine.gecko.ext + +import mozilla.components.concept.storage.Address +import org.mozilla.geckoview.Autocomplete + +/** + * Converts a GeckoView [Autocomplete.Address] to an Android Components [Address]. + */ +fun Autocomplete.Address.toAddress() = Address( + guid = guid, + givenName = givenName, + additionalName = additionalName, + familyName = familyName, + organization = organization, + streetAddress = streetAddress, + addressLevel3 = addressLevel3, + addressLevel2 = addressLevel2, + addressLevel1 = addressLevel1, + postalCode = postalCode, + country = country, + tel = tel, + email = email +) + +/** + * Converts an Android Components [Address] to a GeckoView [Autocomplete.Address]. + */ +fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder() + .guid(guid) + .givenName(givenName) + .additionalName(additionalName) + .familyName(familyName) + .organization(organization) + .streetAddress(streetAddress) + .addressLevel3(addressLevel3) + .addressLevel2(addressLevel2) + .addressLevel1(addressLevel1) + .postalCode(postalCode) + .country(country) + .tel(tel) + .email(email) + .build() diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index fe065d6389c..f410b6dece4 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -8,6 +8,8 @@ import android.content.Context import android.net.Uri import androidx.annotation.VisibleForTesting import mozilla.components.browser.engine.gecko.GeckoEngineSession +import mozilla.components.browser.engine.gecko.ext.toAddress +import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry import mozilla.components.browser.engine.gecko.ext.toLoginEntry @@ -17,6 +19,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry @@ -224,6 +227,39 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe return geckoResult } + override fun onAddressSelect( + session: GeckoSession, + request: AutocompleteRequest + ): GeckoResult? { + val geckoResult = GeckoResult() + + val onConfirm: (Address) -> Unit = { address -> + if (!request.isComplete) { + geckoResult.complete( + request.confirm( + Autocomplete.AddressSelectOption(address.toAutocompleteAddress()) + ) + ) + } + } + + val onDismiss: () -> Unit = { + request.dismissSafely(geckoResult) + } + + geckoEngineSession.notifyObservers { + onPromptRequest( + PromptRequest.SelectAddress( + addresses = request.options.map { it.value.toAddress() }, + onConfirm = onConfirm, + onDismiss = onDismiss + ) + ) + } + + return geckoResult + } + override fun onAlertPrompt( session: GeckoSession, prompt: PromptDelegate.AlertPrompt diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt index 760a17bfa38..abe674d0c53 100644 --- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt +++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Looper.getMainLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.browser.engine.gecko.GeckoEngineSession +import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard import mozilla.components.browser.engine.gecko.ext.toLoginEntry import mozilla.components.concept.engine.EngineSession @@ -15,6 +16,7 @@ import mozilla.components.concept.engine.prompt.Choice import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry @@ -1436,6 +1438,96 @@ class GeckoPromptDelegateTest { verify(geckoResult, never()).complete(any()) } + @Test + fun `WHEN onAddressSelect is called THEN SelectAddress prompt request must be provided with the correct callbacks`() { + val mockSession = GeckoEngineSession(runtime) + + var isOnConfirmCalled = false + var isOnDismissCalled = false + + var selectAddressPrompt: PromptRequest.SelectAddress = mock() + + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + // Capture the SelectAddress prompt request + mockSession.register(object : EngineSession.Observer { + override fun onPromptRequest(promptRequest: PromptRequest) { + selectAddressPrompt = promptRequest as PromptRequest.SelectAddress + } + }) + + val address = Address( + guid = "1", + givenName = "Firefox", + additionalName = "-", + familyName = "-", + organization = "-", + streetAddress = "street", + addressLevel3 = "address3", + addressLevel2 = "address2", + addressLevel1 = "address1", + postalCode = "1", + country = "Country", + tel = "1", + email = "@" + ) + val addressSelectOption = + Autocomplete.AddressSelectOption(address.toAutocompleteAddress()) + + var geckoPrompt = + geckoSelectAddressPrompt(arrayOf(addressSelectOption)) + + var geckoResult = promptDelegate.onAddressSelect( + mock(), + geckoPrompt + ) + + // Verify that the onDismiss callback was called + geckoResult!!.accept { + isOnDismissCalled = true + } + + selectAddressPrompt.onDismiss() + shadowOf(getMainLooper()).idle() + assertTrue(isOnDismissCalled) + + // Verify that the onConfirm callback was called + geckoPrompt = + geckoSelectAddressPrompt(arrayOf(addressSelectOption)) + + geckoResult = promptDelegate.onAddressSelect( + mock(), + geckoPrompt + ) + + geckoResult!!.accept { + isOnConfirmCalled = true + } + + selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first()) + shadowOf(getMainLooper()).idle() + assertTrue(isOnConfirmCalled) + + // Verify that when the request is already completed, the onConfirm callback is called + // but the request is not confirmed + isOnConfirmCalled = false + geckoPrompt = + geckoSelectAddressPrompt(arrayOf(addressSelectOption), true) + + geckoResult = promptDelegate.onAddressSelect( + mock(), + geckoPrompt + ) + + geckoResult!!.accept { + isOnConfirmCalled = true + } + + selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first()) + shadowOf(getMainLooper()).idle() + assertFalse(isOnConfirmCalled) + } + private fun geckoChoicePrompt( title: String, message: String, @@ -1595,4 +1687,15 @@ class GeckoPromptDelegateTest { ReflectionUtils.setField(prompt, "options", creditCards) return prompt } + + private fun geckoSelectAddressPrompt( + addresses: Array, + isComplete: Boolean = false + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt: GeckoSession.PromptDelegate.AutocompleteRequest = + mock() + whenever(prompt.isComplete).thenReturn(isComplete) + ReflectionUtils.setField(prompt, "options", addresses) + return prompt + } } diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt index 68d85c512a8..6fe3846a58e 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt @@ -9,6 +9,7 @@ import android.net.Uri import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry @@ -346,6 +347,21 @@ sealed class PromptRequest( override val onDismiss: () -> Unit ) : PromptRequest(), Dismissible + /** + * Value type that represents a request for a select address prompt. + * + * This prompt is triggered by the user focusing on an address field. + * + * @property addresses List of addresses for the user to choose from. + * @property onConfirm Callback used to confirm the selected address. + * @property onDismiss Callback used to dismiss the address prompt. + */ + data class SelectAddress( + val addresses: List
, + val onConfirm: (Address) -> Unit, + override val onDismiss: () -> Unit + ) : PromptRequest(), Dismissible + interface Dismissible { val onDismiss: () -> Unit } diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt index ef0e3a41540..1caf192128d 100644 --- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt +++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt @@ -20,6 +20,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type +import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry @@ -312,4 +313,52 @@ class PromptRequestTest { assertFalse(onConfirmCalled) assertNull(confirmedCreditCard) } + + @Test + fun `WHEN calling confirm or dismiss on SelectAddress THEN the respective callback is invoked`() { + val address = Address( + guid = "1", + givenName = "Firefox", + additionalName = "-", + familyName = "-", + organization = "-", + streetAddress = "street", + addressLevel3 = "address3", + addressLevel2 = "address2", + addressLevel1 = "address1", + postalCode = "1", + country = "Country", + tel = "1", + email = "@" + ) + var onDismissCalled = false + var onConfirmCalled = false + var confirmedAddress: Address? = null + + val selectAddresPromptRequest = PromptRequest.SelectAddress( + addresses = listOf(address), + onDismiss = { + onDismissCalled = true + }, + onConfirm = { + confirmedAddress = it + onConfirmCalled = true + } + ) + + assertEquals(selectAddresPromptRequest.addresses, listOf(address)) + + selectAddresPromptRequest.onConfirm(address) + + assertTrue(onConfirmCalled) + assertFalse(onDismissCalled) + assertEquals(address, confirmedAddress) + + onConfirmCalled = false + + selectAddresPromptRequest.onDismiss() + + assertTrue(onDismissCalled) + assertFalse(onConfirmCalled) + } } diff --git a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt index 663c03db27e..c1628930797 100644 --- a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt +++ b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt @@ -315,7 +315,7 @@ data class UpdatableCreditCardFields( @SuppressLint("ParcelCreator") @Parcelize data class Address( - val guid: String, + val guid: String? = null, val givenName: String, val additionalName: String, val familyName: String, @@ -328,10 +328,10 @@ data class Address( val country: String, val tel: String, val email: String, - val timeCreated: Long, - val timeLastUsed: Long?, - val timeLastModified: Long, - val timesUsed: Long + val timeCreated: Long = 0L, + val timeLastUsed: Long? = 0L, + val timeLastModified: Long = 0L, + val timesUsed: Long = 0L ) : Parcelable /** diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt index 0f1a58449bc..ee3a5d9d0eb 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt @@ -34,6 +34,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.Popup import mozilla.components.concept.engine.prompt.PromptRequest.Repost import mozilla.components.concept.engine.prompt.PromptRequest.SaveLoginPrompt +import mozilla.components.concept.engine.prompt.PromptRequest.SelectAddress import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt import mozilla.components.concept.engine.prompt.PromptRequest.Share @@ -791,6 +792,7 @@ class PromptFeature private constructor( is SelectLoginPrompt, is SelectCreditCard, is Share -> true + is SelectAddress -> false is Alert, is TextPrompt, is Confirm, is Repost, is Popup -> promptAbuserDetector.shouldShowMoreDialogs } } diff --git a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt index 310d9297a4d..296b0a4dc63 100644 --- a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt +++ b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt @@ -270,7 +270,7 @@ class AutofillCreditCardsAddressesStorageTest { ) val address = storage.addAddress(addressFields) - assertEquals(address, storage.getAddress(address.guid)) + assertEquals(address, storage.getAddress(address.guid!!)) } @Test @@ -370,9 +370,9 @@ class AutofillCreditCardsAddressesStorageTest { email = "mary@example.com" ) - storage.updateAddress(address.guid, newAddressFields) + storage.updateAddress(address.guid!!, newAddressFields) - address = storage.getAddress(address.guid)!! + address = storage.getAddress(address.guid!!)!! assertEquals(newAddressFields.givenName, address.givenName) assertEquals(newAddressFields.additionalName, address.additionalName) @@ -406,12 +406,12 @@ class AutofillCreditCardsAddressesStorageTest { ) val address = storage.addAddress(addressFields) - val savedAddress = storage.getAddress(address.guid) + val savedAddress = storage.getAddress(address.guid!!) assertEquals(address, savedAddress) - val isDeleteSuccessful = storage.deleteAddress(address.guid) + val isDeleteSuccessful = storage.deleteAddress(address.guid!!) assertTrue(isDeleteSuccessful) - assertNull(storage.getAddress(address.guid)) + assertNull(storage.getAddress(address.guid!!)) } @Test diff --git a/docs/changelog.md b/docs/changelog.md index 3bbe1d9cef0..2a81c144ff7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,9 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/main/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/main/.config.yml) +* **concept-engine**: + * Added support for `SelectAddress` prompt request. + * **feature-contextmenu** * 🌟 Add new `additionalValidation` parameter to context menu options builders allowing clients to know when these options to be shown and potentially block showing them.