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..2013e563d75 --- /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.engine.prompt.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..e6dba7c65b8 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,9 +8,12 @@ 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 +import mozilla.components.concept.engine.prompt.Address import mozilla.components.concept.engine.prompt.Choice import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice @@ -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( + addressList = 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..90e9fc33dcb 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,9 +8,11 @@ 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 +import mozilla.components.concept.engine.prompt.Address import mozilla.components.concept.engine.prompt.Choice import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice @@ -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.addressList.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.addressList.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/Address.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Address.kt new file mode 100644 index 00000000000..819c8c9b8b5 --- /dev/null +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/Address.kt @@ -0,0 +1,52 @@ +/* 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.concept.engine.prompt + +import android.annotation.SuppressLint +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Information about an address. + * + * @property guid The unique identifier for this address. + * @property givenName First name. + * @property additionalName Middle name. + * @property familyName Last name. + * @property organization Organization. + * @property streetAddress Street address. + * @property addressLevel3 Sublocality (Suburb) name type. + * @property addressLevel2 Locality (City/Town) name type. + * @property addressLevel1 Province/State name type. + * @property postalCode Postal code. + * @property country Country. + * @property tel Telephone number. + * @property email E-mail address. + * @property timeCreated Time of creation in milliseconds from the unix epoch. + * @property timeLastUsed Time of last use in milliseconds from the unix epoch. + * @property timeLastModified Time of last modified in milliseconds from the unix epoch. + * @property timesUsed Number of times the address was used. + */ +@SuppressLint("ParcelCreator") +@Parcelize +data class Address( + val guid: String?, + val givenName: String, + val additionalName: String, + val familyName: String, + val organization: String, + val streetAddress: String, + val addressLevel3: String, + val addressLevel2: String, + val addressLevel1: String, + val postalCode: String, + val country: String, + val tel: String, + val email: String, + val timeCreated: Long = 0L, + val timeLastUsed: Long? = 0L, + val timeLastModified: Long = 0L, + val timesUsed: Long = 0L +) : Parcelable 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..86f8479381e 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 @@ -346,6 +346,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 addressList 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 addressList: 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..152802564aa 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 @@ -312,4 +312,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( + addressList = listOf(address), + onDismiss = { + onDismissCalled = true + }, + onConfirm = { + confirmedAddress = it + onConfirmCalled = true + } + ) + + assertEquals(selectAddresPromptRequest.addressList, listOf(address)) + + selectAddresPromptRequest.onConfirm(address) + + assertTrue(onConfirmCalled) + assertFalse(onDismissCalled) + assertEquals(address, confirmedAddress) + + onConfirmCalled = false + + selectAddresPromptRequest.onDismiss() + + assertTrue(onDismissCalled) + assertFalse(onConfirmCalled) + } } 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 dc9ded4cf06..1bd1d3bc7c3 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 @@ -782,6 +783,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 } }