diff options
Diffstat (limited to 'chromium/third_party/libaddressinput')
54 files changed, 8479 insertions, 15 deletions
diff --git a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_da.xtb b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_da.xtb index d4915ff5ee2..14f01a42e9c 100644 --- a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_da.xtb +++ b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_da.xtb @@ -33,7 +33,7 @@ <translation id="7087282848513945231">Amt/region</translation> <translation id="7139724024395191329">Emirat</translation> <translation id="7393381084163773901">Adresse</translation> -<translation id="7602447984296396718">Dette postnummer ser ikke ud til at passe til resten af denne adresse.</translation> +<translation id="7602447984296396718">Dette postnummer ser ikke ud til at passe til resten af adressen.</translation> <translation id="7738983109397305830">Dette postnummer stemmer tilsyneladende ikke overens med resten af adressen. Kender du ikke dit postnummer? Find det <ph name="BEGIN_LINK" />her<ph name="END_LINK" />.</translation> <translation id="777702478322588152">Præfektur</translation> <translation id="7805765407568469194">Landsby/township</translation> @@ -42,6 +42,6 @@ <translation id="8446364922515257065">Do/Si</translation> <translation id="8449204988444194299">Postby</translation> <translation id="8471101563037901452">Dette postnummer stemmer tilsyneladende ikke overens med resten af adressen. Kender du ikke dit postnummer? Find det <ph name="BEGIN_LINK" />her<ph name="END_LINK" />.</translation> -<translation id="9104066683700680171">Dette postnummer ser ikke ud til at passe til resten af denne adresse.</translation> +<translation id="9104066683700680171">Dette postnummer ser ikke ud til at passe til resten af adressen.</translation> <translation id="9207002871037636573">Du skal angive et postnummer, f.eks. <ph name="EXAMPLE" />.</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_de.xtb b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_de.xtb index 896356439c1..3fd9e26f2d7 100644 --- a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_de.xtb +++ b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_de.xtb @@ -40,7 +40,7 @@ <translation id="8184169487503545976">Dieses Postleitzahlenformat wird nicht erkannt.</translation> <translation id="820600307078153032">Das Format der Postleitzahl wurde nicht erkannt. Beispiel für eine gültige Postleitzahl: <ph name="EXAMPLE" /></translation> <translation id="8446364922515257065">Do/Si</translation> -<translation id="8449204988444194299">Post Town</translation> +<translation id="8449204988444194299">Stadt</translation> <translation id="8471101563037901452">Die Postleitzahl stimmt offenbar nicht mit dem Rest der Adresse überein. Sie kennen Ihre Postleitzahl nicht? <ph name="BEGIN_LINK" />Schlagen Sie sie hier nach<ph name="END_LINK" />.</translation> <translation id="9104066683700680171">Diese Postleitzahl scheint nicht zum Rest dieser Adresse zu passen.</translation> <translation id="9207002871037636573">Sie müssen eine Postleitzahl angeben, wie beispielsweise <ph name="EXAMPLE" />.</translation> diff --git a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_id.xtb b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_id.xtb index 6ccce4dee4f..aca4bfef5a4 100644 --- a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_id.xtb +++ b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_id.xtb @@ -34,14 +34,14 @@ <translation id="7139724024395191329">Emirat</translation> <translation id="7393381084163773901">Alamat</translation> <translation id="7602447984296396718">Tampaknya kode pos ini tidak cocok dengan sisa alamat ini.</translation> -<translation id="7738983109397305830">Kode postingan ini tampaknya tidak cocok dengan bagian lain dari alamat ini Tidak tahu kode pos Anda? Temukan <ph name="BEGIN_LINK" />di sini<ph name="END_LINK" />.</translation> +<translation id="7738983109397305830">Kode pos ini tampaknya tidak cocok dengan bagian lain dari alamat ini Tidak tahu kode pos Anda? Temukan <ph name="BEGIN_LINK" />di sini<ph name="END_LINK" />.</translation> <translation id="777702478322588152">Prefektur</translation> <translation id="7805765407568469194">Desa/Kecamatan</translation> <translation id="8184169487503545976">Format kode pos ini tidak dikenal.</translation> <translation id="820600307078153032">Format kode pos ini tidak dikenal. Contoh kode pos yang valid: <ph name="EXAMPLE" />.</translation> <translation id="8446364922515257065">Do/Si</translation> <translation id="8449204988444194299">Kota</translation> -<translation id="8471101563037901452">Kode postingan ini tampaknya tidak cocok dengan bagian lain dari alamat ini. Tidak tahu kode pos Anda? Temukan <ph name="BEGIN_LINK" />di sini<ph name="END_LINK" />.</translation> +<translation id="8471101563037901452">Kode pos ini tampaknya tidak cocok dengan bagian lain dari alamat ini. Tidak tahu kode pos Anda? Temukan <ph name="BEGIN_LINK" />di sini<ph name="END_LINK" />.</translation> <translation id="9104066683700680171">Tampaknya kode pos ini tidak cocok dengan sisa alamat ini.</translation> <translation id="9207002871037636573">Anda harus memberikan kode pos, misalnya <ph name="EXAMPLE" />.</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_mr.xtb b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_mr.xtb index b7b16ea0702..e049746ad1c 100644 --- a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_mr.xtb +++ b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_mr.xtb @@ -1,18 +1,18 @@ <?xml version="1.0" ?> <!DOCTYPE translationbundle> <translationbundle lang="mr"> -<translation id="1340068511406764697">आपण एक पोस्टल कोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />. आपला पोस्टल कोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> +<translation id="1340068511406764697">तुम्ही एक पोस्टल कोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />. तुमचा पोस्टल कोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> <translation id="2053553514270667976">पिनकोड</translation> <translation id="2096368010154057602">विभाग</translation> <translation id="2577522251608256362">अतिपरिचित क्षेत्र</translation> -<translation id="3050787670591910834">आपण एक पोस्टल कोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />.</translation> +<translation id="3050787670591910834">तुम्ही एक पोस्टल कोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />.</translation> <translation id="3174168572213147020">बेट</translation> -<translation id="3713769522066937702">हे पिनकोड स्वरूपन ओळखीचे नाही. वैध पिनकोडचे उदाहरण: <ph name="EXAMPLE" />. आपला पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> +<translation id="3713769522066937702">हे पिनकोड स्वरूपन ओळखीचे नाही. वैध पिनकोडचे उदाहरण: <ph name="EXAMPLE" />. तुमचा पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> <translation id="3882422586004212847">हे पोस्टल कोड स्वरूपन ओळखीचे नाही. वैध पोस्टल कोडचे उदाहरण: <ph name="EXAMPLE" />.</translation> <translation id="3885155851504623709">पॅरिश</translation> -<translation id="43113324827158664">आपण हे रिक्त सोडू शकत नाही.</translation> +<translation id="43113324827158664">तुम्ही हे रिक्त सोडू शकत नाही.</translation> <translation id="4376888869070172068">हे पोस्टल कोड स्वरुपन ओळखले गेले नाही.</translation> -<translation id="4518701284698680367">आपण एक पिनकोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />. आपला पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> +<translation id="4518701284698680367">तुम्ही एक पिनकोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />. तुमचा पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> <translation id="5089810972385038852">राज्य</translation> <translation id="5095208057601539847">प्रांत</translation> <translation id="5327248766486351172">नाव</translation> @@ -34,14 +34,14 @@ <translation id="7139724024395191329">अमिरात</translation> <translation id="7393381084163773901">मार्ग पत्ता</translation> <translation id="7602447984296396718">हा उर्वरित पत्ता जुळविण्यासाठी हा पिन कोड दिसत नाही.</translation> -<translation id="7738983109397305830">हा पिनकोड या बाकीच्या पत्त्याशी जुळत नाही. आपला पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> +<translation id="7738983109397305830">हा पिनकोड या बाकीच्या पत्त्याशी जुळत नाही. तुमचा पिनकोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> <translation id="777702478322588152">परफेक्चुअर</translation> <translation id="7805765407568469194">खेडे / उपनगर</translation> <translation id="8184169487503545976">हे पिन कोड स्वरुपन ओळखले गेले नाही.</translation> <translation id="820600307078153032">हे पिनकोड स्वरूपन ओळखीचे नाही. वैध पिनकोडचे उदाहरण: <ph name="EXAMPLE" />.</translation> <translation id="8446364922515257065">Do/Si</translation> <translation id="8449204988444194299">पोस्ट टाउन</translation> -<translation id="8471101563037901452">हा पोस्टल कोड या बाकीच्या पत्त्याशी जुळत नाही. आपला पोस्टल कोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> +<translation id="8471101563037901452">हा पोस्टल कोड या बाकीच्या पत्त्याशी जुळत नाही. तुमचा पोस्टल कोड माहीत नाही? तो <ph name="BEGIN_LINK" />येथे<ph name="END_LINK" /> शोधा.</translation> <translation id="9104066683700680171">हा उर्वरित पत्ता जुळविण्यासाठी हा पोस्टल कोड दिसला नाही.</translation> -<translation id="9207002871037636573">आपण एक पिनकोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />.</translation> +<translation id="9207002871037636573">तुम्ही एक पिनकोड प्रदान करणे आवश्यक आहे, उदाहरणार्थ <ph name="EXAMPLE" />.</translation> </translationbundle>
\ No newline at end of file diff --git a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_sk.xtb b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_sk.xtb index 5fe20ae6447..5a6d0c5fcc0 100644 --- a/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_sk.xtb +++ b/chromium/third_party/libaddressinput/chromium/resources/address_input_strings_sk.xtb @@ -34,10 +34,10 @@ <translation id="7139724024395191329">Emirát</translation> <translation id="7393381084163773901">Ulica</translation> <translation id="7602447984296396718">PSČ zrejme nezodpovedá zvyšnej časti adresy.</translation> -<translation id="7738983109397305830">Toto PSČ zrejme nezodpovedá zvyšku adresy. Nepoznáte svoje PSČ? Zistite ho <ph name="BEGIN_LINK" />tu<ph name="END_LINK" />.</translation> +<translation id="7738983109397305830">Poštové smerovacie číslo nezodpovedá zvyšku adresy. Nepoznáte svoje poštové smerovacie číslo? <ph name="BEGIN_LINK" />Zistite ho tu<ph name="END_LINK" />.</translation> <translation id="777702478322588152">Prefektúra</translation> <translation id="7805765407568469194">Dedina/okres</translation> -<translation id="8184169487503545976">Tento formát PSČ nebol rozpoznaný.</translation> +<translation id="8184169487503545976">Formát poštového smerovacieho čísla nebol rozpoznaný.</translation> <translation id="820600307078153032">Formát tohto PSČ sa nedá rozpoznať. Príklad platného PSČ: <ph name="EXAMPLE" />.</translation> <translation id="8446364922515257065">Do/Si</translation> <translation id="8449204988444194299">Poštový okres</translation> diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/android/util/Log.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/android/util/Log.java new file mode 100644 index 00000000000..71b3e912e72 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/android/util/Log.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 Google Inc. + * + * 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 android.util; + +/** + * Simple mock of Android logger class. + */ +public final class Log { + public static int v(String tag, String msg) { return 0; } + public static int v(String tag, String msg, Throwable tr) { return 0; } + public static int d(String tag, String msg) { return 0; } + public static int d(String tag, String msg, Throwable tr) { return 0; } + public static int i(String tag, String msg) { return 0; } + public static int i(String tag, String msg, Throwable tr) { return 0; } + public static int w(String tag, String msg) { return 0; } + public static int w(String tag, String msg, Throwable tr) { return 0; } + public static boolean isLoggable(String tag, int level) { return false; } + public static int w(String tag, Throwable tr) { return 0; } + public static int e(String tag, String msg) { return 0; } + public static int e(String tag, String msg, Throwable tr) { return 0; } + public static int wtf(String tag, String msg) { return 0; } + public static int wtf(String tag, Throwable tr) { return 0; } + public static int wtf(String tag, String msg, Throwable tr) { return 0; } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java new file mode 100644 index 00000000000..d7898f147aa --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.android.i18n.addressinput; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.database.DataSetObserver; +import android.test.ActivityInstrumentationTestCase2; +import android.widget.AutoCompleteTextView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import com.android.i18n.addressinput.AddressAutocompleteController.AddressAdapter; +import com.android.i18n.addressinput.AddressAutocompleteController.AddressPrediction; +import com.android.i18n.addressinput.testing.TestActivity; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; +import com.google.i18n.addressinput.common.AddressAutocompleteApi; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.OnAddressSelectedListener; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link AddressAutocompleteController}. */ +public class AddressAutocompleteControllerTest + extends ActivityInstrumentationTestCase2<TestActivity> { + private static final String TEST_QUERY = "TEST_QUERY"; + + private Context context; + + private AddressAutocompleteController controller; + private AutoCompleteTextView textView; + + // Mock services + private @Mock AddressAutocompleteApi autocompleteApi; + private @Mock PlaceDetailsApi placeDetailsApi; + + // Mock data + private @Captor ArgumentCaptor<FutureCallback<List<? extends AddressAutocompletePrediction>>> + autocompleteCallback; + private @Mock AddressAutocompletePrediction autocompletePrediction; + + public AddressAutocompleteControllerTest() { + super(TestActivity.class); + } + + @Override + protected void setUp() { + MockitoAnnotations.initMocks(this); + + context = getActivity(); + + textView = new AutoCompleteTextView(context); + controller = + new AddressAutocompleteController(context, autocompleteApi, placeDetailsApi) + .setView(textView); + } + + // Tests for the AddressAutocompleteController + + public void testAddressAutocompleteController() throws InterruptedException, ExecutionException { + final AddressData expectedAddress = AddressData.builder() + .addAddressLine("1600 Amphitheatre Parkway") + .setLocality("Mountain View") + .setAdminArea("California") + .setCountry("US") + .build(); + + Future<AddressData> actualAddress = getAutocompletePredictions(expectedAddress); + + assertEquals(1, textView.getAdapter().getCount()); + assertEquals(actualAddress.get(), expectedAddress); + } + + // Tests for the AddressAdapter + + public void testAddressAdapter_getItem() { + AddressAdapter adapter = new AddressAdapter(context); + List<AddressPrediction> predictions = + Lists.newArrayList(new AddressPrediction(TEST_QUERY, autocompletePrediction)); + + adapter.refresh(predictions); + assertEquals(adapter.getCount(), predictions.size()); + for (int i = 0; i < predictions.size(); i++) { + assertEquals("Item #" + i, predictions.get(0), adapter.getItem(0)); + } + } + + public void testAddressAdapter_getView() { + AddressAdapter adapter = new AddressAdapter(context); + List<AddressPrediction> predictions = + Lists.newArrayList(new AddressPrediction(TEST_QUERY, autocompletePrediction)); + + adapter.refresh(predictions); + for (int i = 0; i < predictions.size(); i++) { + assertNotNull("Item #" + i, adapter.getView(0, null, new LinearLayout(context))); + } + } + + // Helper functions + + private Future<AddressData> getAutocompletePredictions(AddressData expectedAddress) { + // Set up the AddressData to be returned from the AddressAutocompleteApi and PlaceDetailsApi. + when(autocompleteApi.isConfiguredCorrectly()).thenReturn(true); + when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID"); + when(placeDetailsApi.getAddressData(autocompletePrediction)) + .thenReturn(Futures.immediateFuture(expectedAddress)); + + // Perform a click on the first autocomplete suggestion once it is loaded. + textView + .getAdapter() + .registerDataSetObserver( + new DataSetObserver() { + @Override + public void onInvalidated() {} + + @Override + public void onChanged() { + // For some reason, performing a click on the view or dropdown view associated with + // the first item in the list doesn't trigger the onItemClick listener in tests, so + // we trigger it manually here. + textView + .getOnItemClickListener() + .onItemClick(new ListView(context), new TextView(context), 0, 0); + } + }); + + // The OnAddressSelectedListener is the way for the AddressWidget to consume the AddressData + // produced by autocompletion. + final SettableFuture<AddressData> result = SettableFuture.create(); + controller.setOnAddressSelectedListener( + new OnAddressSelectedListener() { + @Override + public void onAddressSelected(AddressData address) { + result.set(address); + } + }); + + // Actually trigger the behaviors mocked above. + textView.setText(TEST_QUERY); + + verify(autocompleteApi) + .getAutocompletePredictions(any(String.class), autocompleteCallback.capture()); + autocompleteCallback.getValue().onSuccess(Lists.newArrayList(autocompletePrediction)); + + return result; + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressWidgetUiComponentProviderTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressWidgetUiComponentProviderTest.java new file mode 100644 index 00000000000..5caa764e13e --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AddressWidgetUiComponentProviderTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2014 Google Inc. + * + * 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.android.i18n.addressinput; + +import android.app.ProgressDialog; +import android.content.Context; +import android.test.ActivityInstrumentationTestCase2; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import com.android.i18n.addressinput.testing.TestActivity; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.AddressField; +import com.google.i18n.addressinput.common.AddressProblemType; +import com.google.i18n.addressinput.common.FormOptions; +import com.google.i18n.addressinput.common.SimpleClientCacheManager; + +/** Test class for {@link AddressWidgetUiComponentProvider}. */ +public class AddressWidgetUiComponentProviderTest + extends ActivityInstrumentationTestCase2<TestActivity> { + private AddressWidget widget; + private AddressWidgetUiComponentProvider componentProvider; + private LinearLayout container; + private AddressData address; + private Context context; + private int customTextViewCounter; + private int customProgressDialogCounter; + + public AddressWidgetUiComponentProviderTest() { + super(TestActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + AddressData.Builder builder = + new AddressData.Builder() + .setCountry("US") + .setLanguageCode("en") + .setAddress("1098 Alta Ave") + .setAdminArea("CA"); + address = builder.build(); + context = getActivity(); + container = new LinearLayout(context); + } + + public void testCustomWidgets() { + customTextViewCounter = 0; + customProgressDialogCounter = 0; + componentProvider = new TestComponentProvider(context); + widget = + new AddressWidget( + context, + container, + new FormOptions(), + new SimpleClientCacheManager(), + componentProvider); + widget.renderFormWithSavedAddress(address); + + for (AddressField field : AddressField.values()) { + if (field.equals(AddressField.COUNTRY)) { + continue; + } + + View view = widget.getViewForField(field); + if (view instanceof EditText) { + assertTrue( + "Field " + field + " does not use customized edit text widget.", + view instanceof CustomEditText); + } else if (view instanceof Spinner) { + assertTrue( + "Field " + field + " does not use customized spinner widget.", + view instanceof CustomSpinner); + assertTrue( + "Field " + field + " does not use customized ArrayAdapter.", + ((Spinner) view).getAdapter() instanceof CustomArrayAdapter); + } + } + + assertTrue("Custom TextView label not used.", customTextViewCounter > 0); + assertTrue("Custom ProgressDialog not used.", customProgressDialogCounter > 0); + } + + public void testDisplayAndClearingOfErrorMessages() { + customTextViewCounter = 0; + customProgressDialogCounter = 0; + componentProvider = new TestComponentProvider(context); + widget = + new AddressWidget( + context, + container, + new FormOptions(), + new SimpleClientCacheManager(), + componentProvider); + widget.renderFormWithSavedAddress(address); + + EditText streetAddressView = (EditText) widget.getViewForField(AddressField.STREET_ADDRESS); + EditText addressLine1View = (EditText) widget.getViewForField(AddressField.ADDRESS_LINE_1); + EditText localityView = (EditText) widget.getViewForField(AddressField.STREET_ADDRESS); + EditText postalCodeView = (EditText) widget.getViewForField(AddressField.POSTAL_CODE); + + // Verify that there are no errors by default. + assertNull(streetAddressView.getError()); + assertNull(addressLine1View.getError()); // Expected to be the same as STREET_ADDRESS + assertNull(localityView.getError()); + assertNull(postalCodeView.getError()); + + // Flag STREET_ADDRESS and LOCALITY with errors + widget.displayErrorMessageForField( + address, AddressField.STREET_ADDRESS, AddressProblemType.MISSING_REQUIRED_FIELD); + widget.displayErrorMessageForField( + address, AddressField.LOCALITY, AddressProblemType.MISSING_REQUIRED_FIELD); + + // Verify that errors have been applied to the fields above, and other remain unchanged. + assertNotNull(streetAddressView.getError()); + assertNotNull(addressLine1View.getError()); // Expected to be the same as STREET_ADDRESS + assertNotNull(localityView.getError()); + + // Verify that postal code does not have an error applied. + assertNull(postalCodeView.getError()); + + // Clear all error messages. + widget.clearErrorMessage(); + + // Verify that all errors have been cleared. + assertNull(streetAddressView.getError()); + assertNull(addressLine1View.getError()); // Expected to be the same as STREET_ADDRESS + assertNull(localityView.getError()); + assertNull(postalCodeView.getError()); + } + + private void increaseTextViewCounter() { + customTextViewCounter++; + } + + private void increaseProgressDialogCounter() { + customProgressDialogCounter++; + } + + private class CustomEditText extends EditText { + CustomEditText(Context context) { + super(context); + } + } + + private class CustomSpinner extends Spinner { + CustomSpinner(Context context) { + super(context); + } + } + + private class CustomArrayAdapter extends ArrayAdapter<String> { + CustomArrayAdapter(Context context, int id) { + super(context, id); + } + } + + private class TestComponentProvider extends AddressWidgetUiComponentProvider { + TestComponentProvider(Context context) { + super(context); + } + + @Override + protected TextView createUiLabel(CharSequence label, AddressField.WidthType widthType) { + TextView result = new TextView(context); + result.setText(label); + AddressWidgetUiComponentProviderTest.this.increaseTextViewCounter(); + return result; + } + + @Override + protected EditText createUiTextField(AddressField.WidthType widthType) { + return new CustomEditText(context); + } + + @Override + protected Spinner createUiPickerSpinner(AddressField.WidthType widthType) { + return new CustomSpinner(context); + } + + @Override + protected ArrayAdapter<String> createUiPickerAdapter(AddressField.WidthType widthType) { + ArrayAdapter<String> result = + new CustomArrayAdapter(context, android.R.layout.simple_spinner_item); + result.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + return result; + } + + @Override + protected ProgressDialog getUiActivityIndicatorView() { + AddressWidgetUiComponentProviderTest.this.increaseProgressDialogCounter(); + return super.getUiActivityIndicatorView(); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java new file mode 100644 index 00000000000..7b558b124f6 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.android.i18n.addressinput.testing.AsyncTestCase; +import com.google.i18n.addressinput.common.AsyncRequestApi; +import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback; +import com.google.i18n.addressinput.common.JsoMap; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +public class AndroidAsyncEncodedRequestApiTest extends AsyncTestCase { + private AsyncRequestApi requestApi; + + @Override + public void setUp() { + requestApi = new AndroidAsyncEncodedRequestApi(); + } + + public void testRequestObject() throws Exception { + delayTestFinish(4000); + + String url = HttpServer.execute(1000, "{\"id\": \"data\"}"); + + requestApi.requestObject(url, new AsyncCallback() { + @Override public void onFailure() { + fail("unexpected failure"); + } + + @Override public void onSuccess(JsoMap result) { + assertNotNull(result); + assertEquals("data", result.get("id")); + finishTest(); + } + }, 2000); + } + + public void testTimeout() throws Exception { + delayTestFinish(4000); + + String url = HttpServer.execute(2000, "Fubar"); + + requestApi.requestObject(url, new AsyncCallback() { + @Override public void onFailure() { + finishTest(); + } + + @Override public void onSuccess(JsoMap result) { + fail("The request should have timed out."); + } + }, 1000); + } + + public void testUrlEncoding() throws Exception { + delayTestFinish(4000); + + String urlBase = HttpServer.execute(1000, "{\"id\": \"data\"}"); + String url = urlBase + "address/data/VN/B\u1EAFc K\u1EA1n"; + + requestApi.requestObject(url, new AsyncCallback() { + @Override public void onFailure() { + fail("unexpected failure"); + } + + @Override public void onSuccess(JsoMap result) { + assertNotNull(result); + assertEquals("data", result.get("id")); + finishTest(); + } + }, 2000); + } + + /** + * Simple implementation of an HTTP server. + */ + private static class HttpServer extends Thread { + /** + * Start an HTTP server that will serve one request and then terminate. + * + * @param timeoutMillis + * Wait this long before answering a request. + * @param response + * Reply to any request with this response. + * @return The URL to the server. + * @throws IOException + */ + public static String execute(long timeoutMillis, String response) throws IOException { + HttpServer server = new HttpServer(timeoutMillis, response); + server.start(); + return "http://localhost:" + server.serverSocket.getLocalPort() + "/"; + } + + @Override + public void run() { + try { + Socket clientSocket = serverSocket.accept(); + try { + synchronized (this) { + wait(waitMillis); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + InputStream inputStream = clientSocket.getInputStream(); + inputStream.read(new byte[1024]); // Discard input. + OutputStream outputStream = clientSocket.getOutputStream(); + outputStream.write(response); + outputStream.close(); + inputStream.close(); + clientSocket.close(); + serverSocket.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private HttpServer(long waitMillis, String response) throws IOException { + this.waitMillis = waitMillis; + this.response = (HEADER + response).getBytes(); + serverSocket = new ServerSocket(0); + } + + private long waitMillis; + private byte[] response; + private ServerSocket serverSocket; + + private static final String HEADER = "HTTP/1.0 200 OK\nContent-Type: text/plain\n\n"; + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AsyncTestCaseTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AsyncTestCaseTest.java new file mode 100644 index 00000000000..36833b8915a --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/AsyncTestCaseTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.android.i18n.addressinput.testing.AsyncTestCase; + +import junit.framework.AssertionFailedError; + +import java.util.concurrent.TimeoutException; + +public class AsyncTestCaseTest extends AsyncTestCase { + + public void testSuccess() { + delayTestFinish(1000); + AsyncCallback.execute(500, new Runnable() { + @Override + public void run() { + finishTest(); + } + }); + } + + public void testFailure() { + expectTimeout = true; + delayTestFinish(1000); + AsyncCallback.execute(1500, new Runnable() { + @Override + public void run() { + finishTest(); + } + }); + } + + @Override + protected void runTest() throws Throwable { + expectTimeout = false; + try { + super.runTest(); + } catch (TimeoutException e) { + if (expectTimeout) { + return; + } else { + throw e; + } + } + if (expectTimeout) { + throw new AssertionFailedError("Test case did not time out."); + } + } + + private boolean expectTimeout; + + /** + * Helper class to perform an asynchronous callback after a specified delay. + */ + private static class AsyncCallback extends Thread { + private long waitMillis; + private Runnable callback; + + private AsyncCallback(long waitMillis, Runnable callback) { + this.waitMillis = waitMillis; + this.callback = callback; + } + + public static void execute(long waitMillis, Runnable callback) { + (new AsyncCallback(waitMillis, callback)).start(); + } + + @Override + public void run() { + try { + synchronized (this) { + wait(this.waitMillis); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + this.callback.run(); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/DummyTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/DummyTest.java new file mode 100644 index 00000000000..f190998c84e --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/DummyTest.java @@ -0,0 +1,15 @@ +package com.android.i18n.addressinput; + +import android.test.ActivityInstrumentationTestCase2; +import com.android.i18n.addressinput.testing.TestActivity; + +/** + * Empty test file. The API level 19 test emulator requires a nonempty dex to run tests. + * This empty file is included in the srcs attribute of the android_test rule so that the real test + * source files can be included via the binary_under_test attribute. + */ +public class DummyTest extends ActivityInstrumentationTestCase2<TestActivity> { + public DummyTest() { + super(TestActivity.class); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java new file mode 100644 index 00000000000..23f40eec8d7 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java @@ -0,0 +1,87 @@ +package com.android.i18n.addressinput; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.test.ActivityInstrumentationTestCase2; +import com.android.i18n.addressinput.testing.TestActivity; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.AsyncRequestApi; +import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback; +import com.google.i18n.addressinput.common.JsoMap; +import java.util.concurrent.ExecutionException; +import org.json.JSONException; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link PlaceDetailsClient}. */ +public class PlaceDetailsClientTest extends ActivityInstrumentationTestCase2<TestActivity> { + @Mock private AsyncRequestApi asyncRequestApi; + @Mock private AddressAutocompletePrediction autocompletePrediction; + + @Captor ArgumentCaptor<AsyncCallback> callbackCaptor; + + private PlaceDetailsClient placeDetailsClient; + + public PlaceDetailsClientTest() { + super(TestActivity.class); + } + + @Override + protected void setUp() { + MockitoAnnotations.initMocks(this); + + placeDetailsClient = new PlaceDetailsClient("TEST_API_KEY", asyncRequestApi); + when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID"); + } + + public void testOnSuccess() throws InterruptedException, ExecutionException, JSONException { + ListenableFuture<AddressData> addressData = + placeDetailsClient.getAddressData(autocompletePrediction); + + verify(asyncRequestApi) + .requestObject(any(String.class), callbackCaptor.capture(), eq(PlaceDetailsClient.TIMEOUT)); + callbackCaptor.getValue().onSuccess(JsoMap.buildJsoMap(TEST_RESPONSE)); + + assertEquals( + AddressData.builder() + .setAddress("1600 Amphitheatre Parkway") + .setLocality("Mountain View") + .setAdminArea("CA") + .setCountry("US") + .setPostalCode("94043") + .build(), + addressData.get()); + } + + public void testOnFailure() { + ListenableFuture<AddressData> addressData = + placeDetailsClient.getAddressData(autocompletePrediction); + + verify(asyncRequestApi) + .requestObject(any(String.class), callbackCaptor.capture(), eq(PlaceDetailsClient.TIMEOUT)); + callbackCaptor.getValue().onFailure(); + + assertTrue(addressData.isCancelled()); + } + + private static final String TEST_RESPONSE = + "{" + + " 'result' : {" + + " 'adr_address' : '\\u003cspan class=\\\"street-address\\\"\\u003e1600 Amphitheatre Parkway\\u003c/span\\u003e, \\u003cspan class=\\\"locality\\\"\\u003eMountain View\\u003c/span\\u003e, \\u003cspan class=\\\"region\\\"\\u003eCA\\u003c/span\\u003e \\u003cspan class=\\\"postal-code\\\"\\u003e94043\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eUSA\\u003c/span\\u003e'," + + " 'address_components' : [" + + " {" + + " 'long_name' : 'United States'," + + " 'short_name' : 'US'," + + " 'types' : [ 'country', 'political' ]" + + " }" + + " ]" + + " }" + + "}"; +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java new file mode 100644 index 00000000000..f0495bf6c69 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.android.i18n.addressinput.autocomplete.gmscore; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.location.Location; +import android.test.ActivityInstrumentationTestCase2; +import com.android.i18n.addressinput.testing.TestActivity; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.PendingResults; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.places.AutocompleteFilter; +import com.google.android.gms.location.places.AutocompletePrediction; +import com.google.android.gms.location.places.AutocompletePredictionBuffer; +import com.google.android.gms.location.places.GeoDataApi; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.SettableFuture; +import com.google.i18n.addressinput.common.AddressAutocompleteApi; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** Unit tests for {@link AddressAutocompleteApi}. */ +public class AddressAutocompleteApiImplTest extends ActivityInstrumentationTestCase2<TestActivity> { + private static final String TAG = "AddrAutoApiTest"; + private static final String TEST_QUERY = "TEST_QUERY"; + + private AddressAutocompleteApi addressAutocompleteApi; + + // Mock services + private GeoDataApi geoDataApi = mock(GeoDataApi.class); + private GoogleApiClient googleApiClient = mock(GoogleApiClient.class); + private FusedLocationProviderApi locationApi = mock(FusedLocationProviderApi.class); + + // Mock data + private AutocompletePredictionBuffer autocompleteResults = + mock(AutocompletePredictionBuffer.class); + private PendingResult<AutocompletePredictionBuffer> autocompletePendingResults = + PendingResults.immediatePendingResult(autocompleteResults); + private AutocompletePrediction autocompletePrediction = mock(AutocompletePrediction.class); + + public AddressAutocompleteApiImplTest() { + super(TestActivity.class); + } + + @Override + protected void setUp() { + addressAutocompleteApi = + new AddressAutocompleteApiImpl(googleApiClient, geoDataApi, locationApi); + } + + // Tests for the AddressAutocompleteApi + + public void testAddressAutocompleteApi() throws InterruptedException, ExecutionException { + when(googleApiClient.isConnected()).thenReturn(true); + when(locationApi.getLastLocation(googleApiClient)).thenReturn(new Location("TEST_PROVIDER")); + + Future<List<? extends AddressAutocompletePrediction>> actualPredictions = + getAutocompleteSuggestions(); + + List<AddressAutocompletePrediction> expectedPredictions = + Lists.newArrayList(new AddressAutocompletePredictionImpl(autocompletePrediction)); + + assertEquals(actualPredictions.get(), expectedPredictions); + } + + public void testAddressAutocompleteApi_deviceLocationMissing() + throws InterruptedException, ExecutionException { + when(googleApiClient.isConnected()).thenReturn(true); + when(locationApi.getLastLocation(googleApiClient)).thenReturn(null); + + Future<List<? extends AddressAutocompletePrediction>> actualPredictions = + getAutocompleteSuggestions(); + + List<AddressAutocompletePrediction> expectedPredictions = + Lists.newArrayList(new AddressAutocompletePredictionImpl(autocompletePrediction)); + + assertEquals(actualPredictions.get(), expectedPredictions); + } + + public void testAddressAutocompleteApi_isConfiguredCorrectly() { + when(googleApiClient.isConnected()).thenReturn(true); + assertTrue(addressAutocompleteApi.isConfiguredCorrectly()); + } + + // Helper functions + + private Future<List<? extends AddressAutocompletePrediction>> getAutocompleteSuggestions() { + // Set up the AddressData to be returned from the PlaceAutocomplete API + PlaceDetailsApi. + // Most of the objects that are mocked here are not services, but simply data without any + // public constructors. + when(geoDataApi.getAutocompletePredictions( + eq(googleApiClient), + eq(TEST_QUERY), + any(LatLngBounds.class), + any(AutocompleteFilter.class))) + .thenReturn(autocompletePendingResults); + when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID"); + when(autocompletePrediction.getFullText(null)).thenReturn("TEST_PREDICTION"); + when(autocompleteResults.iterator()) + .thenReturn(Arrays.asList(autocompletePrediction).iterator()); + when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID"); + when(autocompletePrediction.getPrimaryText(null)).thenReturn("TEST_PRIMARY_ID"); + when(autocompletePrediction.getSecondaryText(null)).thenReturn("TEST_SECONDARY_ID"); + + SettableFuture<List<? extends AddressAutocompletePrediction>> actualPredictions = + SettableFuture.create(); + addressAutocompleteApi.getAutocompletePredictions( + TEST_QUERY, + new FutureCallback<List<? extends AddressAutocompletePrediction>>() { + @Override + public void onSuccess(List<? extends AddressAutocompletePrediction> predictions) { + actualPredictions.set(predictions); + } + + @Override + public void onFailure(Throwable error) { + assertTrue("Error getting autocomplete predictions: " + error.toString(), false); + } + }); + + return actualPredictions; + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/AsyncTestCase.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/AsyncTestCase.java new file mode 100644 index 00000000000..d8a5a4304a5 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/AsyncTestCase.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.android.i18n.addressinput.testing; + +import junit.framework.TestCase; + +import java.util.concurrent.TimeoutException; + +/** + * An extension of TestCase that provides delayTestFinish() and finishTest() methods that behave + * like the corresponding methods in GWTTestCase for testing asynchronous code. + */ +public abstract class AsyncTestCase extends TestCase { + /** + * Tracks whether this test is completely done. + */ + private boolean testIsFinished; + + /** + * The system time in milliseconds when the test should time out. + */ + private long testTimeoutMillis; + + /** + * Puts the current test in asynchronous mode. + * + * @param timeoutMillis time to wait before failing the test for timing out + */ + protected void delayTestFinish(int timeoutMillis) { + testTimeoutMillis = System.currentTimeMillis() + timeoutMillis; + } + + /** + * Causes this test to succeed during asynchronous mode. + */ + protected void finishTest() { + testIsFinished = true; + synchronized (this) { + notify(); + } + } + + @Override + protected void runTest() throws Throwable { + testIsFinished = false; + testTimeoutMillis = 0; + super.runTest(); + + if (testTimeoutMillis > 0) { + long timeoutMillis = testTimeoutMillis - System.currentTimeMillis(); + if (timeoutMillis > 0) { + synchronized (this) { + wait(timeoutMillis); + } + } + if (!testIsFinished) { + throw new TimeoutException("Waited " + timeoutMillis + " ms!"); + } + } + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/TestActivity.java b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/TestActivity.java new file mode 100644 index 00000000000..4e6779ef7f6 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/androidTest/java/com/android/i18n/addressinput/testing/TestActivity.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2014 Google Inc. + * + * 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.android.i18n.addressinput.testing; + +import android.app.Activity; + +public class TestActivity extends Activity {} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java new file mode 100644 index 00000000000..c71eb4e5d77 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.android.i18n.addressinput; + +import android.content.Context; +import android.os.AsyncTask; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.FutureCallback; +import com.google.i18n.addressinput.common.AddressAutocompleteApi; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.OnAddressSelectedListener; +import java.util.ArrayList; +import java.util.List; + +/** Controller for address autocomplete results. */ +class AddressAutocompleteController { + + private static final String TAG = "AddressAutocompleteCtrl"; + + private AddressAutocompleteApi autocompleteApi; + private PlaceDetailsApi placeDetailsApi; + private AddressAdapter adapter; + private OnAddressSelectedListener listener; + + private TextWatcher textChangedListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int after) { + getAddressPredictions(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) {} + }; + + private AdapterView.OnItemClickListener onItemClickListener = + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (listener != null) { + AddressAutocompletePrediction prediction = + (AddressAutocompletePrediction) + adapter.getItem(position).getAutocompletePrediction(); + + (new AsyncTask<AddressAutocompletePrediction, Void, AddressData>() { + @Override + protected AddressData doInBackground( + AddressAutocompletePrediction... predictions) { + try { + return placeDetailsApi.getAddressData(predictions[0]).get(); + } catch (Exception e) { + cancel(true); + Log.i(TAG, "Error getting place details: ", e); + return null; + } + } + + @Override + protected void onPostExecute(AddressData addressData) { + Log.e(TAG, "AddressData: " + addressData.toString()); + listener.onAddressSelected(addressData); + } + }) + .execute(prediction); + } else { + Log.i(TAG, "No onAddressSelected listener."); + } + } + }; + + AddressAutocompleteController( + Context context, AddressAutocompleteApi autocompleteApi, PlaceDetailsApi placeDetailsApi) { + this.placeDetailsApi = placeDetailsApi; + this.autocompleteApi = autocompleteApi; + + adapter = new AddressAdapter(context); + } + + AddressAutocompleteController setView(AutoCompleteTextView textView) { + textView.setAdapter(adapter); + textView.setOnItemClickListener(onItemClickListener); + textView.addTextChangedListener(textChangedListener); + + return this; + } + + AddressAutocompleteController setOnAddressSelectedListener(OnAddressSelectedListener listener) { + this.listener = listener; + return this; + } + + void getAddressPredictions(final String query) { + if (!autocompleteApi.isConfiguredCorrectly()) { + return; + } + + autocompleteApi.getAutocompletePredictions( + query, + new FutureCallback<List<? extends AddressAutocompletePrediction>>() { + @Override + public void onSuccess(List<? extends AddressAutocompletePrediction> predictions) { + List<AddressPrediction> wrappedPredictions = new ArrayList<>(); + + for (AddressAutocompletePrediction prediction : predictions) { + wrappedPredictions.add(new AddressPrediction(query, prediction)); + } + + adapter.refresh(wrappedPredictions); + } + + @Override + public void onFailure(Throwable error) { + Log.i(TAG, "Error getting autocomplete predictions: ", error); + } + }); + } + + @VisibleForTesting + static class AddressPrediction { + private String prefix; + private AddressAutocompletePrediction autocompletePrediction; + + AddressPrediction(String prefix, AddressAutocompletePrediction prediction) { + this.prefix = prefix; + this.autocompletePrediction = prediction; + } + + String getPrefix() { + return prefix; + }; + + AddressAutocompletePrediction getAutocompletePrediction() { + return autocompletePrediction; + }; + + @Override + public final String toString() { + return getPrefix(); + } + } + + // The main purpose of this custom adapter is the custom getView function. + // This adapter extends BaseAdapter instead of ArrayAdapter because ArrayAdapter has a filtering + // bug that is triggered by the AutoCompleteTextView (see + // http://www.jaysoyer.com/2014/07/filtering-problems-arrayadapter/). + @VisibleForTesting + static class AddressAdapter extends BaseAdapter implements Filterable { + private Context context; + + private List<AddressPrediction> predictions; + + AddressAdapter(Context context) { + this.context = context; + this.predictions = new ArrayList<AddressPrediction>(); + } + + public AddressAdapter refresh(List<AddressPrediction> newPredictions) { + predictions = newPredictions; + notifyDataSetChanged(); + + return this; + } + + @Override + public int getCount() { + return predictions.size(); + } + + @Override + public AddressPrediction getItem(int position) { + return predictions.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + // No-op filter. + // Results from the PlaceAutocomplete API don't need to be filtered any further. + @Override + public Filter getFilter() { + return new Filter() { + @Override + public Filter.FilterResults performFiltering(CharSequence constraint) { + Filter.FilterResults results = new Filter.FilterResults(); + results.count = predictions.size(); + results.values = predictions; + + return results; + } + + @Override + public void publishResults(CharSequence constraint, Filter.FilterResults results) {} + }; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout view = + convertView instanceof LinearLayout + ? (LinearLayout) convertView + : (LinearLayout) + inflater.inflate(R.layout.address_autocomplete_dropdown_item, parent, false); + AddressPrediction prediction = predictions.get(position); + + TextView line1 = (TextView) view.findViewById(R.id.line_1); + if (line1 != null) { + line1.setText(prediction.getAutocompletePrediction().getPrimaryText()); + } + + TextView line2 = (TextView) view.findViewById(R.id.line_2); + line2.setText(prediction.getAutocompletePrediction().getSecondaryText()); + + return view; + } + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java new file mode 100644 index 00000000000..c1d11a4775b --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.google.i18n.addressinput.common.AddressField; +import com.google.i18n.addressinput.common.RegionData; + +import android.view.View; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.Spinner; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a component in the address widget UI. It could be either a text box (when there is no + * candidate) or a spinner. + */ +class AddressUiComponent { + // The label for the UI component + private String fieldName; + + // The type of the UI component + private UiComponent uiType; + + // The list of elements in the UI component + private List<RegionData> candidatesList = new ArrayList<RegionData>(); + + // The id of this UI component + private AddressField id; + + // The id of the parent UI component. When the parent UI component is updated, this UI + // component should be updated. + private AddressField parentId; + + // The View representing the UI component + private View view; + + /** + * Type of UI component. There are only EDIT (text-box) and SPINNER (drop-down) components. + */ + enum UiComponent { + EDIT, SPINNER, + } + + AddressUiComponent(AddressField id) { + this.id = id; + // By default, an AddressUiComponent doesn't depend on anything else. + this.parentId = null; + this.uiType = UiComponent.EDIT; + } + + /** + * Initializes the candidatesList, and set the uiType and parentId. + * @param candidatesList + */ + void initializeCandidatesList(List<RegionData> candidatesList) { + this.candidatesList = candidatesList; + if (candidatesList.size() > 1) { + uiType = UiComponent.SPINNER; + switch (id) { + case DEPENDENT_LOCALITY: + parentId = AddressField.LOCALITY; + break; + case LOCALITY: + parentId = AddressField.ADMIN_AREA; + break; + case ADMIN_AREA: + parentId = AddressField.COUNTRY; + break; + default: + // Ignore. + } + } + } + + /** + * Gets the value entered in the UI component. + */ + String getValue() { + if (view == null) { + return (candidatesList.size() == 0) ? "" : candidatesList.get(0).getDisplayName(); + } + switch (uiType) { + case SPINNER: + Object selectedItem = ((Spinner) view).getSelectedItem(); + if (selectedItem == null) { + return ""; + } + return selectedItem.toString(); + case EDIT: + return ((EditText) view).getText().toString(); + default: + return ""; + } + } + + /** + * Sets the value displayed in the input field. + */ + void setValue(String value) { + if (view == null) { + return; + } + + switch(uiType) { + case SPINNER: + for (int i = 0; i < candidatesList.size(); i++) { + // Assumes that the indices in the candidate list are the same as those used in the + // Adapter backing the Spinner. + if (candidatesList.get(i).getKey().equals(value)) { + ((Spinner) view).setSelection(i); + } + } + return; + case EDIT: + if (view instanceof AutoCompleteTextView) { + // Prevent the AutoCompleteTextView from showing the dropdown. + ((AutoCompleteTextView) view).setText(value, false); + } else { + ((EditText) view).setText(value); + } + return; + default: + return; + } + } + + String getFieldName() { + return fieldName; + } + + void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + UiComponent getUiType() { + return uiType; + } + + void setUiType(UiComponent uiType) { + this.uiType = uiType; + } + + List<RegionData> getCandidatesList() { + return candidatesList; + } + + void setCandidatesList(List<RegionData> candidatesList) { + this.candidatesList = candidatesList; + } + + AddressField getId() { + return id; + } + + void setId(AddressField id) { + this.id = id; + } + + AddressField getParentId() { + return parentId; + } + + void setParentId(AddressField parentId) { + this.parentId = parentId; + } + + void setView(View view) { + this.view = view; + } + + View getView() { + return view; + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java new file mode 100644 index 00000000000..0ce8c826301 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java @@ -0,0 +1,936 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.android.i18n.addressinput; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Handler; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.Spinner; +import android.widget.TextView; +import com.android.i18n.addressinput.AddressUiComponent.UiComponent; +import com.google.i18n.addressinput.common.AddressAutocompleteApi; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.AddressDataKey; +import com.google.i18n.addressinput.common.AddressField; +import com.google.i18n.addressinput.common.AddressField.WidthType; +import com.google.i18n.addressinput.common.AddressProblemType; +import com.google.i18n.addressinput.common.AddressProblems; +import com.google.i18n.addressinput.common.AddressVerificationNodeData; +import com.google.i18n.addressinput.common.CacheData; +import com.google.i18n.addressinput.common.ClientCacheManager; +import com.google.i18n.addressinput.common.ClientData; +import com.google.i18n.addressinput.common.DataLoadListener; +import com.google.i18n.addressinput.common.FieldVerifier; +import com.google.i18n.addressinput.common.FormController; +import com.google.i18n.addressinput.common.FormOptions; +import com.google.i18n.addressinput.common.FormatInterpreter; +import com.google.i18n.addressinput.common.LookupKey; +import com.google.i18n.addressinput.common.LookupKey.KeyType; +import com.google.i18n.addressinput.common.LookupKey.ScriptType; +import com.google.i18n.addressinput.common.OnAddressSelectedListener; +import com.google.i18n.addressinput.common.RegionData; +import com.google.i18n.addressinput.common.StandardAddressVerifier; +import com.google.i18n.addressinput.common.Util; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Address widget that lays out address fields, validate and format addresses according to local + * customs. + */ +public class AddressWidget implements AdapterView.OnItemSelectedListener { + + private Context context; + + private ViewGroup rootView; + + private CacheData cacheData; + + private ClientData clientData; + + // A map for all address fields. + // TODO(dbeaumont): Fix this to avoid needing to map specific address lines. + private final EnumMap<AddressField, AddressUiComponent> inputWidgets = + new EnumMap<AddressField, AddressUiComponent>(AddressField.class); + + private FormController formController; + + private FormatInterpreter formatInterpreter; + + private FormOptions.Snapshot formOptions; + + private StandardAddressVerifier verifier; + + private ProgressDialog progressDialog; + + private String currentRegion; + + private boolean autocompleteEnabled = false; + + private AddressAutocompleteController autocompleteController; + + // The current language the widget uses in BCP47 format. It differs from the default locale of + // the phone in that it contains information on the script to use. + private String widgetLocale; + + private ScriptType script; + + // Possible labels that could be applied to the admin area field of the current country. + // Examples include "state", "province", "emirate", etc. + private static final Map<String, Integer> ADMIN_LABELS; + // Possible labels that could be applied to the locality (city) field of the current country. + // Examples include "city" or "district". + private static final Map<String, Integer> LOCALITY_LABELS; + // Possible labels that could be applied to the sublocality field of the current country. + // Examples include "suburb" or "neighborhood". + private static final Map<String, Integer> SUBLOCALITY_LABELS; + + private static final FormOptions.Snapshot SHOW_ALL_FIELDS = new FormOptions().createSnapshot(); + + // The appropriate label that should be applied to the zip code field of the current country. + private enum ZipLabel { + ZIP, + POSTAL, + PIN, + EIRCODE + } + + private ZipLabel zipLabel; + + static { + Map<String, Integer> adminLabelMap = new HashMap<String, Integer>(15); + adminLabelMap.put("area", R.string.i18n_area); + adminLabelMap.put("county", R.string.i18n_county); + adminLabelMap.put("department", R.string.i18n_department); + adminLabelMap.put("district", R.string.i18n_district); + adminLabelMap.put("do_si", R.string.i18n_do_si); + adminLabelMap.put("emirate", R.string.i18n_emirate); + adminLabelMap.put("island", R.string.i18n_island); + adminLabelMap.put("oblast", R.string.i18n_oblast); + adminLabelMap.put("parish", R.string.i18n_parish); + adminLabelMap.put("prefecture", R.string.i18n_prefecture); + adminLabelMap.put("province", R.string.i18n_province); + adminLabelMap.put("state", R.string.i18n_state); + ADMIN_LABELS = Collections.unmodifiableMap(adminLabelMap); + + Map<String, Integer> localityLabelMap = new HashMap<String, Integer>(2); + localityLabelMap.put("city", R.string.i18n_locality_label); + localityLabelMap.put("district", R.string.i18n_district); + localityLabelMap.put("post_town", R.string.i18n_post_town); + localityLabelMap.put("suburb", R.string.i18n_suburb); + LOCALITY_LABELS = Collections.unmodifiableMap(localityLabelMap); + + Map<String, Integer> sublocalityLabelMap = new HashMap<String, Integer>(2); + sublocalityLabelMap.put("suburb", R.string.i18n_suburb); + sublocalityLabelMap.put("district", R.string.i18n_district); + sublocalityLabelMap.put("neighborhood", R.string.i18n_neighborhood); + sublocalityLabelMap.put("village_township", R.string.i18n_village_township); + sublocalityLabelMap.put("townland", R.string.i18n_townland); + SUBLOCALITY_LABELS = Collections.unmodifiableMap(sublocalityLabelMap); + } + + // Need handler for callbacks to the UI thread + final Handler handler = new Handler(); + + final Runnable updateMultipleFields = + new Runnable() { + @Override + public void run() { + updateFields(); + } + }; + + private class UpdateRunnable implements Runnable { + private AddressField myId; + + public UpdateRunnable(AddressField id) { + myId = id; + } + + @Override + public void run() { + updateInputWidget(myId); + } + } + + private static class AddressSpinnerInfo { + private Spinner view; + + private AddressField id; + + private AddressField parentId; + + private ArrayAdapter<String> adapter; + + private List<RegionData> currentRegions; + + @SuppressWarnings("unchecked") + public AddressSpinnerInfo(Spinner view, AddressField id, AddressField parentId) { + this.view = view; + this.id = id; + this.parentId = parentId; + this.adapter = (ArrayAdapter<String>) view.getAdapter(); + } + + public void setSpinnerList(List<RegionData> list, String defaultKey) { + currentRegions = list; + adapter.clear(); + for (RegionData item : list) { + adapter.add(item.getDisplayName()); + } + adapter.sort(Collator.getInstance(Locale.getDefault())); + if (defaultKey.length() == 0) { + view.setSelection(0); + } else { + int position = adapter.getPosition(defaultKey); + view.setSelection(position); + } + } + + // Returns the region key of the currently selected region in the Spinner. + public String getRegionCode(int position) { + if (adapter.getCount() <= position) { + return ""; + } + String value = adapter.getItem(position); + return getRegionDataKeyForValue(value); + } + + // Returns the region key for the region value. + public String getRegionDataKeyForValue(String value) { + for (RegionData data : currentRegions) { + if (data.getDisplayName().equals(value)) { + return data.getKey(); + } + } + for (RegionData data : currentRegions) { + if (data.getDisplayName().endsWith(value)) { + return data.getKey(); + } + } + return ""; + } + } + + private final ArrayList<AddressSpinnerInfo> spinners = new ArrayList<AddressSpinnerInfo>(); + + private AddressWidgetUiComponentProvider componentProvider; + + private WidthType getFieldWidthType(AddressUiComponent field) { + // TODO(user): For drop-downs (spinners), derive the width-type from the list of values. + return field.getId().getWidthTypeForRegion(currentRegion); + } + + private void createView( + ViewGroup rootView, AddressUiComponent field, String defaultKey, boolean readOnly) { + @SuppressWarnings("deprecation") // FILL_PARENT renamed MATCH_PARENT in API Level 8. + LinearLayout.LayoutParams lp = + new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); + String fieldText = field.getFieldName(); + WidthType widthType = getFieldWidthType(field); + + if (fieldText.length() > 0) { + TextView textView = componentProvider.createUiLabel(fieldText, widthType); + rootView.addView(textView, lp); + } + if (field.getUiType().equals(UiComponent.EDIT)) { + if (autocompleteEnabled && field.getId() == AddressField.ADDRESS_LINE_1) { + AutoCompleteTextView autocomplete = + componentProvider.createUiAutoCompleteTextField(widthType); + autocomplete.setEnabled(!readOnly); + autocompleteController.setView(autocomplete); + autocompleteController.setOnAddressSelectedListener( + new OnAddressSelectedListener() { + @Override + public void onAddressSelected(AddressData addressData) { + // Autocompletion will never return the recipient or the organization, so we don't + // want to overwrite those fields. We copy the recipient and organization fields + // over to avoid this. + AddressData current = AddressWidget.this.getAddressData(); + AddressWidget.this.renderFormWithSavedAddress( + AddressData.builder(addressData) + .setRecipient(current.getRecipient()) + .setOrganization(current.getOrganization()) + .build()); + } + }); + field.setView(autocomplete); + rootView.addView(autocomplete, lp); + } else { + EditText editText = componentProvider.createUiTextField(widthType); + field.setView(editText); + editText.setEnabled(!readOnly); + rootView.addView(editText, lp); + } + } else if (field.getUiType().equals(UiComponent.SPINNER)) { + ArrayAdapter<String> adapter = componentProvider.createUiPickerAdapter(widthType); + Spinner spinner = componentProvider.createUiPickerSpinner(widthType); + + field.setView(spinner); + spinner.setEnabled(!readOnly); + rootView.addView(spinner, lp); + spinner.setAdapter(adapter); + AddressSpinnerInfo spinnerInfo = + new AddressSpinnerInfo(spinner, field.getId(), field.getParentId()); + spinnerInfo.setSpinnerList(field.getCandidatesList(), defaultKey); + + if (fieldText.length() > 0) { + spinner.setPrompt(fieldText); + } + spinner.setOnItemSelectedListener(this); + spinners.add(spinnerInfo); + } + } + + private void createViewForCountry() { + if (!formOptions.isHidden(AddressField.COUNTRY)) { + // For initialization when the form is first created. + if (!inputWidgets.containsKey(AddressField.COUNTRY)) { + buildCountryListBox(); + } + createView( + rootView, + inputWidgets.get(AddressField.COUNTRY), + getLocalCountryName(currentRegion), + formOptions.isReadonly(AddressField.COUNTRY)); + } + } + + /** Associates each field with its corresponding AddressUiComponent. */ + private void buildFieldWidgets() { + AddressData data = new AddressData.Builder().setCountry(currentRegion).build(); + LookupKey key = new LookupKey.Builder(LookupKey.KeyType.DATA).setAddressData(data).build(); + AddressVerificationNodeData countryNode = clientData.getDefaultData(key.toString()); + + // Set up AddressField.ADMIN_AREA + AddressUiComponent adminAreaUi = new AddressUiComponent(AddressField.ADMIN_AREA); + adminAreaUi.setFieldName(getAdminAreaFieldName(countryNode)); + inputWidgets.put(AddressField.ADMIN_AREA, adminAreaUi); + + // Set up AddressField.LOCALITY + AddressUiComponent localityUi = new AddressUiComponent(AddressField.LOCALITY); + localityUi.setFieldName(getLocalityFieldName(countryNode)); + inputWidgets.put(AddressField.LOCALITY, localityUi); + + // Set up AddressField.DEPENDENT_LOCALITY + AddressUiComponent subLocalityUi = new AddressUiComponent(AddressField.DEPENDENT_LOCALITY); + subLocalityUi.setFieldName(getSublocalityFieldName(countryNode)); + inputWidgets.put(AddressField.DEPENDENT_LOCALITY, subLocalityUi); + + // Set up AddressField.ADDRESS_LINE_1 + AddressUiComponent addressLine1Ui = new AddressUiComponent(AddressField.ADDRESS_LINE_1); + addressLine1Ui.setFieldName(context.getString(R.string.i18n_address_line1_label)); + inputWidgets.put(AddressField.ADDRESS_LINE_1, addressLine1Ui); + // Setup an alternate mapping for the first address line which is what validation expects + inputWidgets.put(AddressField.STREET_ADDRESS, addressLine1Ui); + + // Set up AddressField.ADDRESS_LINE_2 + AddressUiComponent addressLine2Ui = new AddressUiComponent(AddressField.ADDRESS_LINE_2); + addressLine2Ui.setFieldName(""); + inputWidgets.put(AddressField.ADDRESS_LINE_2, addressLine2Ui); + + // Set up AddressField.ORGANIZATION + AddressUiComponent organizationUi = new AddressUiComponent(AddressField.ORGANIZATION); + organizationUi.setFieldName(context.getString(R.string.i18n_organization_label)); + inputWidgets.put(AddressField.ORGANIZATION, organizationUi); + + // Set up AddressField.RECIPIENT + AddressUiComponent recipientUi = new AddressUiComponent(AddressField.RECIPIENT); + recipientUi.setFieldName(context.getString(R.string.i18n_recipient_label)); + inputWidgets.put(AddressField.RECIPIENT, recipientUi); + + // Set up AddressField.POSTAL_CODE + AddressUiComponent postalCodeUi = new AddressUiComponent(AddressField.POSTAL_CODE); + postalCodeUi.setFieldName(getZipFieldName(countryNode)); + inputWidgets.put(AddressField.POSTAL_CODE, postalCodeUi); + + // Set up AddressField.SORTING_CODE + AddressUiComponent sortingCodeUi = new AddressUiComponent(AddressField.SORTING_CODE); + sortingCodeUi.setFieldName("CEDEX"); + inputWidgets.put(AddressField.SORTING_CODE, sortingCodeUi); + } + + private void initializeDropDowns() { + AddressUiComponent adminAreaUi = inputWidgets.get(AddressField.ADMIN_AREA); + List<RegionData> adminAreaList = getRegionData(AddressField.COUNTRY); + adminAreaUi.initializeCandidatesList(adminAreaList); + + AddressUiComponent localityUi = inputWidgets.get(AddressField.LOCALITY); + List<RegionData> localityList = getRegionData(AddressField.ADMIN_AREA); + localityUi.initializeCandidatesList(localityList); + } + + // ZIP code is called postal code in some countries, and PIN code in India. This method returns + // the appropriate name for the given countryNode. + private String getZipFieldName(AddressVerificationNodeData countryNode) { + String zipName; + String zipType = countryNode.get(AddressDataKey.ZIP_NAME_TYPE); + if (zipType == null || zipType.equals("postal")) { + zipLabel = ZipLabel.POSTAL; + zipName = context.getString(R.string.i18n_postal_code_label); + } else if (zipType.equals("eircode")) { + zipLabel = ZipLabel.EIRCODE; + zipName = context.getString(R.string.i18n_eir_code_label); + } else if (zipType.equals("pin")) { + zipLabel = ZipLabel.PIN; + zipName = context.getString(R.string.i18n_pin_code_label); + } else { + zipLabel = ZipLabel.ZIP; + zipName = context.getString(R.string.i18n_zip_code_label); + } + return zipName; + } + + private String getLocalityFieldName(AddressVerificationNodeData countryNode) { + String localityLabelType = countryNode.get(AddressDataKey.LOCALITY_NAME_TYPE); + Integer result = LOCALITY_LABELS.get(localityLabelType); + if (result == null) { + // Fallback to city. + result = R.string.i18n_locality_label; + } + return context.getString(result); + } + + private String getSublocalityFieldName(AddressVerificationNodeData countryNode) { + String sublocalityLabelType = countryNode.get(AddressDataKey.SUBLOCALITY_NAME_TYPE); + Integer result = SUBLOCALITY_LABELS.get(sublocalityLabelType); + if (result == null) { + // Fallback to suburb. + result = R.string.i18n_suburb; + } + return context.getString(result); + } + + private String getAdminAreaFieldName(AddressVerificationNodeData countryNode) { + String adminLabelType = countryNode.get(AddressDataKey.STATE_NAME_TYPE); + Integer result = ADMIN_LABELS.get(adminLabelType); + if (result == null) { + // Fallback to province. + result = R.string.i18n_province; + } + return context.getString(result); + } + + private void buildCountryListBox() { + // Set up AddressField.COUNTRY + AddressUiComponent countryUi = new AddressUiComponent(AddressField.COUNTRY); + countryUi.setFieldName(context.getString(R.string.i18n_country_or_region_label)); + ArrayList<RegionData> countries = new ArrayList<RegionData>(); + for (RegionData regionData : + formController.getRegionData(new LookupKey.Builder(KeyType.DATA).build())) { + String regionKey = regionData.getKey(); + Log.i(this.toString(), "Looking at regionKey: " + regionKey); + // ZZ represents an unknown region code. + if (!regionKey.equals("ZZ") && !formOptions.isBlacklistedRegion(regionKey)) { + Log.i(this.toString(), "Adding " + regionKey); + String localCountryName = getLocalCountryName(regionKey); + RegionData country = + new RegionData.Builder().setKey(regionKey).setName(localCountryName).build(); + countries.add(country); + } + } + countryUi.initializeCandidatesList(countries); + inputWidgets.put(AddressField.COUNTRY, countryUi); + } + + private String getLocalCountryName(String regionCode) { + return (new Locale("", regionCode)).getDisplayCountry(Locale.getDefault()); + } + + private AddressSpinnerInfo findSpinnerByView(View view) { + for (AddressSpinnerInfo spinnerInfo : spinners) { + if (spinnerInfo.view == view) { + return spinnerInfo; + } + } + return null; + } + + private void updateFields() { + removePreviousViews(); + createViewForCountry(); + buildFieldWidgets(); + initializeDropDowns(); + layoutAddressFields(); + } + + private void removePreviousViews() { + if (rootView == null) { + return; + } + rootView.removeAllViews(); + } + + private void layoutAddressFields() { + for (AddressField field : formatInterpreter.getAddressFieldOrder(script, currentRegion)) { + if (!formOptions.isHidden(field)) { + createView(rootView, inputWidgets.get(field), "", formOptions.isReadonly(field)); + } + } + } + + private void updateChildNodes(AdapterView<?> parent, int position) { + AddressSpinnerInfo spinnerInfo = findSpinnerByView(parent); + if (spinnerInfo == null) { + return; + } + + // Find all the child spinners, if any, that depend on this one. + final AddressField myId = spinnerInfo.id; + if (myId != AddressField.COUNTRY + && myId != AddressField.ADMIN_AREA + && myId != AddressField.LOCALITY) { + // Only a change in the three AddressFields above will trigger a change in other + // AddressFields. Therefore, for all other AddressFields, we return immediately. + return; + } + + String regionCode = spinnerInfo.getRegionCode(position); + if (myId == AddressField.COUNTRY) { + updateWidgetOnCountryChange(regionCode); + return; + } + + formController.requestDataForAddress( + getAddressData(), + new DataLoadListener() { + @Override + public void dataLoadingBegin() {} + + @Override + public void dataLoadingEnd() { + Runnable updateChild = new UpdateRunnable(myId); + handler.post(updateChild); + } + }); + } + + public void updateWidgetOnCountryChange(String regionCode) { + if (currentRegion.equalsIgnoreCase(regionCode)) { + return; + } + currentRegion = regionCode; + formController.setCurrentCountry(currentRegion); + renderForm(); + } + + private void updateInputWidget(AddressField myId) { + for (AddressSpinnerInfo child : spinners) { + if (child.parentId == myId) { + List<RegionData> candidates = getRegionData(child.parentId); + child.setSpinnerList(candidates, ""); + } + } + } + + public void renderForm() { + createViewForCountry(); + setWidgetLocaleAndScript(); + AddressData data = + new AddressData.Builder().setCountry(currentRegion).setLanguageCode(widgetLocale).build(); + formController.requestDataForAddress( + data, + new DataLoadListener() { + @Override + public void dataLoadingBegin() { + progressDialog = componentProvider.getUiActivityIndicatorView(); + progressDialog.setMessage(context.getString(R.string.address_data_loading)); + Log.d(this.toString(), "Progress dialog started."); + } + + @Override + public void dataLoadingEnd() { + Log.d(this.toString(), "Data loading completed."); + progressDialog.dismiss(); + Log.d(this.toString(), "Progress dialog stopped."); + handler.post(updateMultipleFields); + } + }); + } + + private void setWidgetLocaleAndScript() { + widgetLocale = Util.getWidgetCompatibleLanguageCode(Locale.getDefault(), currentRegion); + formController.setLanguageCode(widgetLocale); + script = Util.isExplicitLatinScript(widgetLocale) ? ScriptType.LATIN : ScriptType.LOCAL; + } + + private List<RegionData> getRegionData(AddressField parentField) { + AddressData address = getAddressData(); + + // Removes language code from address if it is default. This address is used to build + // lookup key, which neglects default language. For example, instead of "data/US--en/CA", + // the right lookup key is "data/US/CA". + if (formController.isDefaultLanguage(address.getLanguageCode())) { + address = AddressData.builder(address).setLanguageCode(null).build(); + } + + LookupKey parentKey = + formController.getDataKeyFor(address).getKeyForUpperLevelField(parentField); + List<RegionData> candidates; + // Can't build a key with parent field, quit. + if (parentKey == null) { + Log.w( + this.toString(), + "Can't build key with parent field " + + parentField + + ". One of" + + " the ancestor fields might be empty"); + + // Removes candidates that exist from previous settings. For example, data/US has a + // list of candidates AB, BC, CA, etc, that list should be cleaned up when user updates + // the address by changing country to Channel Islands. + candidates = new ArrayList<RegionData>(1); + } else { + candidates = formController.getRegionData(parentKey); + } + return candidates; + } + + /** + * Creates an AddressWidget to be attached to rootView for the specific context using the default + * UI component provider. + */ + public AddressWidget( + Context context, + ViewGroup rootView, + FormOptions formOptions, + ClientCacheManager cacheManager) { + this( + context, + rootView, + formOptions, + cacheManager, + new AddressWidgetUiComponentProvider(context)); + } + + /** + * Creates an AddressWidget to be attached to rootView for the specific context using UI component + * provided by the provider. + */ + public AddressWidget( + Context context, + ViewGroup rootView, + FormOptions formOptions, + ClientCacheManager cacheManager, + AddressWidgetUiComponentProvider provider) { + componentProvider = provider; + currentRegion = + ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)) + .getSimCountryIso() + .toUpperCase(Locale.US); + if (currentRegion.length() == 0) { + currentRegion = "US"; + } + init(context, rootView, formOptions.createSnapshot(), cacheManager); + renderForm(); + } + + /** + * Creates an AddressWidget to be attached to rootView for the specific context using the default + * UI component provider, and fill out the address form with savedAddress. + */ + public AddressWidget( + Context context, + ViewGroup rootView, + FormOptions formOptions, + ClientCacheManager cacheManager, + AddressData savedAddress) { + this( + context, + rootView, + formOptions, + cacheManager, + savedAddress, + new AddressWidgetUiComponentProvider(context)); + } + + /** + * Creates an AddressWidget to be attached to rootView for the specific context using UI component + * provided by the provider, and fill out the address form with savedAddress. + */ + public AddressWidget( + Context context, + ViewGroup rootView, + FormOptions formOptions, + ClientCacheManager cacheManager, + AddressData savedAddress, + AddressWidgetUiComponentProvider provider) { + componentProvider = provider; + currentRegion = savedAddress.getPostalCountry(); + // Postal country must be 2 letter country code. Otherwise default to US. + if (currentRegion == null || currentRegion.length() != 2) { + currentRegion = "US"; + } else { + currentRegion = currentRegion.toUpperCase(); + } + init(context, rootView, formOptions.createSnapshot(), cacheManager); + renderFormWithSavedAddress(savedAddress); + } + + /* + * Enables autocompletion for the ADDRESS_LINE_1 field. With autocompletion enabled, the user + * will see suggested addresses in a dropdown menu below the ADDRESS_LINE_1 field as they are + * typing, and when they select an address, the form fields will be autopopulated with the + * selected address. + * + * NOTE: This feature is currently experimental. + * + * If the AddressAutocompleteApi is not configured correctly, then the AddressWidget will degrade + * gracefully to an ordinary plain text input field without autocomplete. + */ + public void enableAutocomplete( + AddressAutocompleteApi autocompleteApi, PlaceDetailsApi placeDetailsApi) { + AddressAutocompleteController autocompleteController = + new AddressAutocompleteController(context, autocompleteApi, placeDetailsApi); + if (autocompleteApi.isConfiguredCorrectly()) { + this.autocompleteEnabled = true; + this.autocompleteController = autocompleteController; + + // The autocompleteEnabled variable set above is used in createView to determine whether to + // use an EditText or an AutoCompleteTextView. Re-rendering the form here ensures that + // createView is called with the updated value of autocompleteEnabled. + renderFormWithSavedAddress(getAddressData()); + } else { + Log.w( + this.toString(), + "Autocomplete not configured correctly, falling back to a plain text " + "input field."); + } + } + + public void disableAutocomplete() { + this.autocompleteEnabled = false; + } + + public void renderFormWithSavedAddress(AddressData savedAddress) { + setWidgetLocaleAndScript(); + removePreviousViews(); + createViewForCountry(); + buildFieldWidgets(); + initializeDropDowns(); + layoutAddressFields(); + initializeFieldsWithAddress(savedAddress); + } + + private void initializeFieldsWithAddress(AddressData savedAddress) { + for (AddressField field : formatInterpreter.getAddressFieldOrder(script, currentRegion)) { + String value = savedAddress.getFieldValue(field); + if (value == null) { + value = ""; + } + + AddressUiComponent uiComponent = inputWidgets.get(field); + if (uiComponent != null) { + uiComponent.setValue(value); + } + } + } + + private void init( + Context context, + ViewGroup rootView, + FormOptions.Snapshot formOptions, + ClientCacheManager cacheManager) { + this.context = context; + this.rootView = rootView; + this.formOptions = formOptions; + // Inject Adnroid specific async request implementation here. + this.cacheData = new CacheData(cacheManager, new AndroidAsyncEncodedRequestApi()); + this.clientData = new ClientData(cacheData); + this.formController = new FormController(clientData, widgetLocale, currentRegion); + this.formatInterpreter = new FormatInterpreter(formOptions); + this.verifier = new StandardAddressVerifier(new FieldVerifier(clientData)); + } + + /** + * Sets address data server URL. Input URL cannot be null. + * + * @param url The service URL. + */ + // TODO: Remove this method and set the URL in the constructor or via the cacheData directly. + public void setUrl(String url) { + cacheData.setUrl(url); + } + + /** Gets user input address in AddressData format. */ + public AddressData getAddressData() { + AddressData.Builder builder = new AddressData.Builder(); + builder.setCountry(currentRegion); + for (AddressField field : formatInterpreter.getAddressFieldOrder(script, currentRegion)) { + AddressUiComponent addressUiComponent = inputWidgets.get(field); + if (addressUiComponent != null) { + String value = addressUiComponent.getValue(); + if (addressUiComponent.getUiType() == UiComponent.SPINNER) { + // For drop-downs, return the key of the region selected instead of the value. + View view = getViewForField(field); + AddressSpinnerInfo spinnerInfo = findSpinnerByView(view); + if (spinnerInfo != null) { + value = spinnerInfo.getRegionDataKeyForValue(value); + } + } + builder.set(field, value); + } + } + builder.setLanguageCode(widgetLocale); + return builder.build(); + } + + /** + * Gets the formatted address. + * + * <p>This method does not validate addresses. Also, it will "normalize" the result strings by + * removing redundant spaces and empty lines. + * + * @return the formatted address + */ + public List<String> getEnvelopeAddress() { + return getEnvelopeAddress(getAddressData()); + } + + /** Gets the formatted address based on the AddressData passed in. */ + public List<String> getEnvelopeAddress(AddressData address) { + return getEnvelopeAddress(formatInterpreter, address); + } + + /** + * Helper function for getting the formatted address based on the FormatInterpreter and + * AddressData passed in. + */ + private static List<String> getEnvelopeAddress( + FormatInterpreter interpreter, AddressData address) { + String countryCode = address.getPostalCountry(); + if (countryCode.length() != 2) { + return Collections.emptyList(); + } + // Avoid crashes due to lower-case country codes (leniency at the input). + String upperCountryCode = countryCode.toUpperCase(); + if (!countryCode.equals(upperCountryCode)) { + address = AddressData.builder(address).setCountry(upperCountryCode).build(); + } + return interpreter.getEnvelopeAddress(address); + } + + /** + * Gets the formatted address based on the AddressData passed in with none of the relevant fields + * hidden. + */ + public static List<String> getFullEnvelopeAddress(AddressData address) { + return getEnvelopeAddress(new FormatInterpreter(SHOW_ALL_FIELDS), address); + } + + /** Get problems found in the address data entered by the user. */ + public AddressProblems getAddressProblems() { + AddressProblems problems = new AddressProblems(); + AddressData addressData = getAddressData(); + verifier.verify(addressData, problems); + return problems; + } + + /** + * Displays an appropriate error message for an AddressField with a problem. + * + * @return the View object representing the AddressField. + */ + public View displayErrorMessageForField( + AddressData address, AddressField field, AddressProblemType problem) { + Log.d(this.toString(), "Display error message for the field: " + field.toString()); + AddressUiComponent addressUiComponent = inputWidgets.get(field); + if (addressUiComponent != null && addressUiComponent.getUiType() == UiComponent.EDIT) { + EditText view = (EditText) addressUiComponent.getView(); + view.setError(getErrorMessageForInvalidEntry(address, field, problem)); + return view; + } + return null; + } + + public String getErrorMessageForInvalidEntry( + AddressData address, AddressField field, AddressProblemType problem) { + switch (problem) { + case MISSING_REQUIRED_FIELD: + return context.getString(R.string.i18n_missing_required_field); + case UNKNOWN_VALUE: + return context.getString(R.string.unknown_entry); + case INVALID_FORMAT: + // We only support this error type for the Postal Code field. + if (zipLabel == ZipLabel.POSTAL) { + return context.getString(R.string.unrecognized_format_postal_code); + } else if (zipLabel == ZipLabel.PIN) { + return context.getString(R.string.unrecognized_format_pin_code); + } else { + return context.getString(R.string.unrecognized_format_zip_code); + } + case MISMATCHING_VALUE: + // We only support this error type for the Postal Code field. + if (zipLabel == ZipLabel.POSTAL) { + return context.getString(R.string.mismatching_value_postal_code); + } else if (zipLabel == ZipLabel.PIN) { + return context.getString(R.string.mismatching_value_pin_code); + } else { + return context.getString(R.string.mismatching_value_zip_code); + } + case UNEXPECTED_FIELD: + throw new IllegalStateException("unexpected problem type: " + problem); + default: + throw new IllegalStateException("unknown problem type: " + problem); + } + } + + /** Clears all error messages in the UI. */ + public void clearErrorMessage() { + for (AddressField field : formatInterpreter.getAddressFieldOrder(script, currentRegion)) { + AddressUiComponent addressUiComponent = inputWidgets.get(field); + + if (addressUiComponent != null && addressUiComponent.getUiType() == UiComponent.EDIT) { + EditText view = (EditText) addressUiComponent.getView(); + if (view != null) { + view.setError(null); + } + } + } + } + + public View getViewForField(AddressField field) { + AddressUiComponent component = inputWidgets.get(field); + if (component == null) { + return null; + } + return component.getView(); + } + + @Override + public void onNothingSelected(AdapterView<?> arg0) {} + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + updateChildNodes(parent, position); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java new file mode 100644 index 00000000000..6931847e945 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.google.i18n.addressinput.common.AddressField.WidthType; + +import android.app.ProgressDialog; +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +/** + * Base class for customizing widgets for address input. + * <p> + * Clients can optionally override this class and pass it to {@link AddressWidget} the constructor. + * This will be invoked by the widget to create UI components that provide consistent look-and-feel + * with other UI components clients might use alongside the address widget. + */ +public class AddressWidgetUiComponentProvider { + protected Context context; + protected LayoutInflater inflater; + + public AddressWidgetUiComponentProvider(Context context) { + this.context = context; + this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Creates a label, e.g. "State", for an address input field. + * + * @param label the label of the address input field + * @param widthType {@link WidthType} of the field + * @return a custom {@link TextView} created for the field + */ + protected TextView createUiLabel(CharSequence label, WidthType widthType) { + TextView textView = (TextView) inflater.inflate(R.layout.address_textview, null, false); + textView.setText(label); + return textView; + } + + /** + * Creates a text input view for an address input field. + * + * @param widthType {@link WidthType} of the field + * @return a custom {@link EditText} created for the field + */ + protected EditText createUiTextField(WidthType widthType) { + return (EditText) inflater.inflate(R.layout.address_edittext, null, false); + } + + /** + * Creates a {@link Spinner} for a input field that uses UI picker. + * + * @param widthType {@link WidthType} of the field + * @return a custom {@link Spinner} created for the field + */ + protected Spinner createUiPickerSpinner(WidthType widthType) { + return (Spinner) inflater.inflate(R.layout.address_spinner, null, false); + } + + /** + * Creates an {@link AutoCompleteTextView} for an input field that uses autocomplete. + * + * @param widthType {@link WidthType} of the field + * @return a custom {@link AutoCompleteTextView} created for the field + */ + protected AutoCompleteTextView createUiAutoCompleteTextField(WidthType widthType) { + return (AutoCompleteTextView) + inflater.inflate(R.layout.address_autocomplete_textview, null, false); + } + + /** + * Creates an {@link ArrayAdapter} to work with the custom {@link Spinner} of a input field that + * uses UI picker. + * + * @param widthType {@link WidthType} of the field + * @return a custom {@link ArrayAdapter} for the field + */ + protected ArrayAdapter<String> createUiPickerAdapter(WidthType widthType) { + ArrayAdapter<String> adapter = + new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + return adapter; + } + + /** Gets an activity indicator to show that a task is in progress. */ + protected ProgressDialog getUiActivityIndicatorView() { + return new ProgressDialog(context); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java new file mode 100644 index 00000000000..bc7305c611f --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java @@ -0,0 +1,46 @@ +package com.android.i18n.addressinput; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +public class AndroidAsyncEncodedRequestApi extends AndroidAsyncRequestApi { + /** + * A quick hack to transform a string into an RFC 3986 compliant URL. + * + * <p>TODO: Refactor the code to stop passing URLs around as strings, to eliminate the need for + * this broken hack. + */ + @Override + protected URL stringToUrl(String url) throws MalformedURLException { + int length = url.length(); + StringBuilder tmp = new StringBuilder(length); + + try { + for (int i = 0; i < length; i++) { + int j = i; + char c = '\0'; + for (; j < length; j++) { + c = url.charAt(j); + if (c == ':' || c == '/') { + break; + } + } + if (j == length) { + tmp.append(URLEncoder.encode(url.substring(i), "UTF-8")); + break; + } else if (j > i) { + tmp.append(URLEncoder.encode(url.substring(i, j), "UTF-8")); + tmp.append(c); + i = j; + } else { + tmp.append(c); + } + } + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // Impossible. + } + return new URL(tmp.toString()); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java new file mode 100644 index 00000000000..5fe40e934a3 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.google.i18n.addressinput.common.AsyncRequestApi; +import com.google.i18n.addressinput.common.JsoMap; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Provider; +import java.security.Security; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; + +/** + * Android implementation of AsyncRequestApi. + * <p> + * Note that this class uses a thread-per-request approach to asynchronous connections. There are + * likely to be better ways of doing this (and even if not, this implementation suffers from several + * issues regarding interruption and cancellation). Ultimately this class should be revisited and + * most likely rewritten. + */ +// TODO: Reimplement this class according to current best-practice for asynchronous requests. +public class AndroidAsyncRequestApi implements AsyncRequestApi { + private static final String TAG = "AsyncRequestApi"; + + /** Simple implementation of asynchronous HTTP GET. */ + private static class AsyncHttp extends Thread { + private final URL requestUrl; + private final AsyncCallback callback; + private final int timeoutMillis; + + protected AsyncHttp(URL requestUrl, AsyncCallback callback, int timeoutMillis) { + this.requestUrl = requestUrl; + this.callback = callback; + this.timeoutMillis = timeoutMillis; + } + + @Override + public void run() { + try { + // While MalformedURLException from URL's constructor is a different kind of error than + // issues with the HTTP request, we're handling them the same way because the URLs are often + // generated based on data returned by previous HTTP requests and we need robust, graceful + // handling of any issues. + HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); + connection.setConnectTimeout(timeoutMillis); + connection.setReadTimeout(timeoutMillis); + + Provider[] providers = Security.getProviders(); + if (providers.length > 0 && providers[0].getName().equals("GmsCore_OpenSSL")) { + // GMS security provider requires special handling of HTTPS connections. (b/29555362) + if (connection instanceof HttpsURLConnection) { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null /* KeyManager */, null /* TrustManager */, null /* SecureRandom */); + SSLSocketFactory sslSocketFactory = context.getSocketFactory(); + if (sslSocketFactory != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory); + } + } + } + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + BufferedReader responseReader = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "UTF-8")); + StringBuilder responseJson = new StringBuilder(); + String line; + while ((line = responseReader.readLine()) != null) { + responseJson.append(line); + } + responseReader.close(); + callback.onSuccess(JsoMap.buildJsoMap(responseJson.toString())); + } else { + callback.onFailure(); + } + connection.disconnect(); + } catch (Exception e) { + callback.onFailure(); + } + } + } + + @Override public void requestObject(String url, AsyncCallback callback, int timeoutMillis) { + try { + (new AsyncHttp(stringToUrl(url), callback, timeoutMillis)).start(); + } catch (MalformedURLException e) { + callback.onFailure(); + } + } + + protected URL stringToUrl(String url) throws MalformedURLException { + return new URL(url); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java new file mode 100644 index 00000000000..3bbb6177407 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.android.i18n.addressinput; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import com.google.i18n.addressinput.common.AddressData; + +/** + * An interface for transforming an {@link AddressAutocompletePrediction} into {@link AddressData}. + */ +public interface PlaceDetailsApi { + ListenableFuture<AddressData> getAddressData(AddressAutocompletePrediction prediction); +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java new file mode 100644 index 00000000000..da7dcf78a47 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java @@ -0,0 +1,162 @@ +package com.android.i18n.addressinput; + +import android.net.Uri; +import android.util.Log; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import com.google.i18n.addressinput.common.AddressData; +import com.google.i18n.addressinput.common.AsyncRequestApi; +import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback; +import com.google.i18n.addressinput.common.JsoMap; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Implementation of the PlaceDetailsApi using the Place Details Web API + * (https://developers.google.com/places/web-service/details). Unfortunately, the Google Place + * Details API for Android does not include a structured representation of the address. + */ +public class PlaceDetailsClient implements PlaceDetailsApi { + + private AsyncRequestApi asyncRequestApi; + private String apiKey; + + @VisibleForTesting static final int TIMEOUT = 5000; + + private static final String TAG = "PlaceDetailsClient"; + + public PlaceDetailsClient(String apiKey, AsyncRequestApi asyncRequestApi) { + this.asyncRequestApi = asyncRequestApi; + this.apiKey = apiKey; + } + + @Override + public ListenableFuture<AddressData> getAddressData(AddressAutocompletePrediction prediction) { + final SettableFuture<AddressData> addressData = SettableFuture.create(); + + asyncRequestApi.requestObject( + new Uri.Builder() + .scheme("https") + .authority("maps.googleapis.com") + .path("maps/api/place/details/json") + .appendQueryParameter("key", apiKey) + .appendQueryParameter("placeid", prediction.getPlaceId()) + .build() + .toString(), + new AsyncCallback() { + @Override + public void onFailure() { + addressData.cancel(false); + } + + @Override + public void onSuccess(JsoMap response) { + // Can't use JSONObject#getJSONObject to get the 'result' because #getJSONObject calls + // #get, which has been broken by JsoMap to only return String values + // *grinds teeth in frustration*. + try { + if (response.has("result")) { + Object result = response.getObject("result"); + if (result instanceof JSONObject) { + addressData.set(getAddressData((JSONObject) result)); + Log.i(TAG, "Successfully set AddressData from Place Details API response."); + } else { + Log.e( + TAG, + "Error parsing JSON response from Place Details API: " + + "expected 'result' field to be a JSONObject."); + onFailure(); + } + } else if (response.has("error_message")) { + String error_message = response.getString("error_message"); + Log.e(TAG, "Place Details API request failed: " + error_message); + } else { + Log.e( + TAG, + "Expected either result or error_message in response " + + "from Place Details API"); + } + } catch (JSONException e) { + Log.e(TAG, "Error parsing JSON response from Place Details API", e); + onFailure(); + } + } + }, + TIMEOUT); + + return addressData; + } + + private AddressData getAddressData(JSONObject result) throws JSONException { + AddressData.Builder addressData = AddressData.builder(); + + // Get the country code from address_components. + JSONArray addressComponents = result.getJSONArray("address_components"); + // Iterate backwards since country is usually at the end. + for (int i = addressComponents.length() - 1; i >= 0; i--) { + JSONObject addressComponent = addressComponents.getJSONObject(i); + + List<String> types = new ArrayList<>(); + JSONArray componentTypes = addressComponent.getJSONArray("types"); + for (int j = 0; j < componentTypes.length(); j++) { + types.add(componentTypes.getString(j)); + } + + if (types.contains("country")) { + addressData.setCountry(addressComponent.getString("short_name")); + break; + } + } + + String unescapedAdrAddress = + result + .getString("adr_address") + .replace("\\\"", "\"") + .replace("\\u003c", "<") + .replace("\\u003e", ">"); + + Pattern adrComponentPattern = Pattern.compile("[, ]{0,2}<span class=\"(.*)\">(.*)<"); + + for (String adrComponent : unescapedAdrAddress.split("/span>")) { + Matcher m = adrComponentPattern.matcher(adrComponent); + Log.i(TAG, adrComponent + " matches: " + m.matches()); + if (m.matches() && m.groupCount() == 2) { + String key = m.group(1); + String value = m.group(2); + switch (key) { + case "street-address": + addressData.setAddress(value); + // TODO(b/33790911): Include the 'extended-address' and 'post-office-box' adr_address + // fields in the AddressData address. + break; + case "locality": + addressData.setLocality(value); + break; + case "region": + addressData.setAdminArea(value); + break; + case "postal-code": + addressData.setPostalCode(value); + break; + case "country-name": + // adr_address country names are not in CLDR format, which is the format used by the + // AddressWidget. We fetch the country code from the address_components instead. + break; + default: + Log.e(TAG, "Key " + key + " not recognized in Place Details API response."); + } + } else { + Log.e(TAG, "Failed to match " + adrComponent + " with regexp " + m.pattern().toString()); + } + } + + return addressData.build(); + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java new file mode 100644 index 00000000000..72580167db4 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java @@ -0,0 +1,107 @@ +package com.android.i18n.addressinput.autocomplete.gmscore; + +import android.location.Location; +import android.util.Log; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.places.AutocompleteFilter; +import com.google.android.gms.location.places.AutocompletePrediction; +import com.google.android.gms.location.places.AutocompletePredictionBuffer; +import com.google.android.gms.location.places.GeoDataApi; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.common.util.concurrent.FutureCallback; +import com.google.i18n.addressinput.common.AddressAutocompleteApi; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; +import java.util.ArrayList; +import java.util.List; + +/** + * GMSCore implementation of {@link com.google.i18n.addressinput.common.AddressAutocompleteApi}. + * + * <p>Internal clients should use the gcoreclient implementation instead. + * + * Callers should provide a GoogleApiClient with the Places.GEO_DATA_API and + * LocationServices.API enabled. The GoogleApiClient should be connected before + * it is passed to AddressWidget#enableAutocomplete. The caller will also need to request the + * following permissions in their AndroidManifest.xml: + * + * <pre> + * <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/> + * <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + * </pre> + * + * Callers should check that the required permissions are actually present. + * TODO(b/32559817): Handle permission check in libaddressinput so that callers don't need to. + */ +public class AddressAutocompleteApiImpl implements AddressAutocompleteApi { + + private static final String TAG = "GmsCoreAddrAutocmplt"; + private GoogleApiClient googleApiClient; + + // Use Places.GeoDataApi. + private GeoDataApi geoDataApi; + + // Use LocationServices.FusedLocationApi. + private FusedLocationProviderApi locationApi; + + public AddressAutocompleteApiImpl( + GoogleApiClient googleApiClient, + GeoDataApi geoDataApi, + FusedLocationProviderApi locationApi) { + this.googleApiClient = googleApiClient; + this.geoDataApi = geoDataApi; + this.locationApi = locationApi; + } + + // TODO(b/32559817): Add a check to ensure that the required permissions have been granted. + @Override + public boolean isConfiguredCorrectly() { + if (!googleApiClient.isConnected()) { + Log.e(TAG, "Cannot get autocomplete predictions because Google API client is not connected."); + return false; + } + + return true; + } + + @Override + public void getAutocompletePredictions( + String query, final FutureCallback<List<? extends AddressAutocompletePrediction>> callback) { + Location deviceLocation = locationApi.getLastLocation(googleApiClient); + LatLngBounds bounds = + deviceLocation == null + ? null + : LatLngBounds.builder() + .include(new LatLng(deviceLocation.getLatitude(), deviceLocation.getLongitude())) + .build(); + + geoDataApi + .getAutocompletePredictions( + googleApiClient, + query, + bounds, + new AutocompleteFilter.Builder() + .setTypeFilter(AutocompleteFilter.TYPE_FILTER_ADDRESS) + .build()) + .setResultCallback( + new ResultCallback<AutocompletePredictionBuffer>() { + @Override + public void onResult(AutocompletePredictionBuffer resultBuffer) { + callback.onSuccess(convertPredictions(resultBuffer)); + } + }); + } + + private List<? extends AddressAutocompletePrediction> convertPredictions( + AutocompletePredictionBuffer resultBuffer) { + List<AddressAutocompletePrediction> predictions = new ArrayList<>(); + + for (AutocompletePrediction prediction : resultBuffer) { + predictions.add(new AddressAutocompletePredictionImpl(prediction)); + } + + return predictions; + } +} diff --git a/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java new file mode 100644 index 00000000000..d38b450d8d2 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java @@ -0,0 +1,32 @@ +package com.android.i18n.addressinput.autocomplete.gmscore; + +import com.google.android.gms.location.places.AutocompletePrediction; +import com.google.i18n.addressinput.common.AddressAutocompletePrediction; + +/** + * GMSCore implementation of {@link + * com.google.i18n.addressinput.common.AddressAutocompletePrediction}. + */ +public class AddressAutocompletePredictionImpl extends AddressAutocompletePrediction { + + private AutocompletePrediction prediction; + + AddressAutocompletePredictionImpl(AutocompletePrediction prediction) { + this.prediction = prediction; + } + + @Override + public String getPlaceId() { + return prediction.getPlaceId(); + } + + @Override + public CharSequence getPrimaryText() { + return prediction.getPrimaryText(null); + } + + @Override + public CharSequence getSecondaryText() { + return prediction.getSecondaryText(null); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java new file mode 100644 index 00000000000..1dd4711df6f --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java @@ -0,0 +1,27 @@ +package com.google.i18n.addressinput.common; + +import com.google.common.util.concurrent.FutureCallback; +import java.util.List; + +/** + * AddressAutocompleteApi encapsulates the functionality required to fetch address autocomplete + * suggestions for an unstructured address query string entered by the user. + * + * An implementation using GMSCore is provided under + * libaddressinput/android/src/main.java/com/android/i18n/addressinput/autocomplete/gmscore. + */ +public interface AddressAutocompleteApi { + /** + * Returns true if the AddressAutocompleteApi is properly configured to fetch autocomplete + * predictions. This allows the caller to enable autocomplete only if the AddressAutocompleteApi + * is properly configured (e.g. the user has granted all the necessary permissions). + */ + boolean isConfiguredCorrectly(); + + /** + * Given an unstructured address query, getAutocompletePredictions fetches autocomplete + * suggestions for the intended address and provides these suggestions via the callback. + */ + void getAutocompletePredictions( + String query, FutureCallback<List<? extends AddressAutocompletePrediction>> callback); +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java new file mode 100644 index 00000000000..6f788a9be6d --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java @@ -0,0 +1,51 @@ +package com.google.i18n.addressinput.common; + +import java.util.Objects; + +/** + * AddressAutocompletePrediction represents an autocomplete suggestion. + * + * Concrete inheriting classes must provide implementations of {@link #getPlaceId}, {@link + * #getPrimaryText}, and {@link #getSecondaryText}. An implementation using GMSCore is provided + * under libaddressinput/android/src/main.java/com/android/i18n/addressinput/autocomplete/gmscore. + */ +public abstract class AddressAutocompletePrediction { + /** + * Returns the place ID of the predicted place. A place ID is a textual identifier that uniquely + * identifies a place, which you can use to retrieve the Place object again later (for example, + * with Google's Place Details Web API). + */ + public abstract String getPlaceId(); + + /** + * Returns the main text describing a place. This is usually the name of the place. Examples: + * "Eiffel Tower", and "123 Pitt Street". + */ + public abstract CharSequence getPrimaryText(); + + /** + * Returns the subsidiary text of a place description. This is useful, for example, as a second + * line when showing autocomplete predictions. Examples: "Avenue Anatole France, Paris, France", + * and "Sydney, New South Wales". + */ + public abstract CharSequence getSecondaryText(); + + // equals and hashCode overridden for testing. + + @Override + public boolean equals(Object o) { + if (!(o instanceof AddressAutocompletePrediction)) { + return false; + } + AddressAutocompletePrediction p = (AddressAutocompletePrediction) o; + + return getPlaceId().equals(p.getPlaceId()) + && getPrimaryText().equals(p.getPrimaryText()) + && getSecondaryText().equals(p.getSecondaryText()); + } + + @Override + public int hashCode() { + return Objects.hash(getPlaceId(), getPrimaryText(), getSecondaryText()); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java new file mode 100644 index 00000000000..e30a89accd6 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java @@ -0,0 +1,789 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import static com.google.i18n.addressinput.common.Util.checkNotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An immutable data structure for international postal addresses, built using the nested + * {@link Builder} class. + * <p> + * Addresses may seem simple, but even within the US there are many quirks (hyphenated street + * addresses, etc.), and internationally addresses vary a great deal. The most sane and complete in + * many ways is the OASIS "extensible Address Language", xAL, which is a published and documented + * XML schema:<br> + * <a href="http://www.oasis-open.org/committees/ciq/download.shtml"> + * http://www.oasis-open.org/committees/ciq/download.shtml</a> + * <p> + * An example address: + * <pre> + * postalCountry: US + * streetAddress: 1098 Alta Ave + * adminstrativeArea: CA + * locality: Mountain View + * dependentLocality: + * postalCode: 94043 + * sortingCode: + * organization: Google + * recipient: Chen-Kang Yang + * language code: en + * </pre> + * <p> + * When using this class it's advised to do as little pre- or post-processing of the fields as + * possible. Typically we expect instances of this class to be used by the address widget and then + * transmitted or converted to other representations using the standard conversion libraries or + * formatted using one of the supported formatters. Attempting to infer semantic information from + * the values of the fields in this class is generally a bad idea. + * <p> + * Specifically the {@link #getFieldValue(AddressField)} is a problematic API as it encourages the + * belief that it is semantically correct to iterate over the fields in order. In general it should + * not be necessary to iterate over the fields in this class; instead use just the specific getter + * methods for the fields you need. + * <p> + * There are valid use cases for examining individual fields, but these are almost always region + * dependent: + * <ul> + * <li>Finding the region of the address. This is really the only completely safe field you can + * examine which gives an unambiguous and well defined result under all circumstances. + * <li>Testing if two addresses have the same administrative area. This is only reliable if the + * data was entered via a drop-down menu, and the size of administrative areas varies greatly + * between and within countries so it doesn't infer much about locality. + * <li>Extracting postal codes or sorting codes for address validation or external lookup. This + * only works for certain countries, such as the United Kingdom, where postal codes have a high + * resolution. + * </ul> + * <p> + * All values stored in this class are trimmed of ASCII whitespace. Setting an empty, or whitespace + * only field in the builder will clear it and result in a {@code null} being returned from the + * corresponding {@code AddressData} instance.. + */ +// This is an external class and part of the widget's public API. +// TODO: Review public API for external classes and tidy JavaDoc. +public final class AddressData { + // The list of deprecated address fields which are superseded by STREET_ADDRESS. + @SuppressWarnings("deprecation") // For legacy address fields. + private static final List<AddressField> ADDRESS_LINE_FIELDS = Collections.unmodifiableList( + Arrays.asList(AddressField.ADDRESS_LINE_1, AddressField.ADDRESS_LINE_2)); + + private static final int ADDRESS_LINE_COUNT = ADDRESS_LINE_FIELDS.size(); + + // The set of address fields for which a single string value can be mapped. + private static final EnumSet<AddressField> SINGLE_VALUE_FIELDS; + static { + SINGLE_VALUE_FIELDS = EnumSet.allOf(AddressField.class); + SINGLE_VALUE_FIELDS.removeAll(ADDRESS_LINE_FIELDS); + SINGLE_VALUE_FIELDS.remove(AddressField.STREET_ADDRESS); + } + + // When this is merged for use by GWT, remember to add @NonFinalForGwt in place of final fields. + + // Detailed information on these fields is available in the javadoc for their respective getters. + + // CLDR (Common Locale Data Repository) country code. + private final String postalCountry; + + // The most specific part of any address. They may be left empty if more detailed fields are used + // instead, or they may be used in addition to these if the more detailed fields do not fulfill + // requirements, or they may be used instead of more detailed fields to represent the street-level + // part. + private final List<String> addressLines; + + // Top-level administrative subdivision of this country. + private final String administrativeArea; + + // Generally refers to the city/town portion of an address. + private final String locality; + + // Dependent locality or sublocality. Used for neighborhoods or suburbs. + private final String dependentLocality; + + // Values are frequently alphanumeric. + private final String postalCode; + + // This corresponds to the SortingCode sub-element of the xAL PostalServiceElements element. + // Use is very country-specific. + private final String sortingCode; + + // The firm or organization. This goes at a finer granularity than address lines in the address. + private final String organization; + + // The recipient. This goes at a finer granularity than address lines in the address. Not present + // in xAL. + private final String recipient; + + // BCP-47 language code for the address. Can be set to null. + private final String languageCode; + + // NOTE: If you add a new field which is semantically significant, you must also add a check for + // that field in {@link equals} and {@link hashCode}. + + private AddressData(Builder builder) { + this.postalCountry = builder.fields.get(AddressField.COUNTRY); + this.administrativeArea = builder.fields.get(AddressField.ADMIN_AREA); + this.locality = builder.fields.get(AddressField.LOCALITY); + this.dependentLocality = builder.fields.get(AddressField.DEPENDENT_LOCALITY); + this.postalCode = builder.fields.get(AddressField.POSTAL_CODE); + this.sortingCode = builder.fields.get(AddressField.SORTING_CODE); + this.organization = builder.fields.get(AddressField.ORGANIZATION); + this.recipient = builder.fields.get(AddressField.RECIPIENT); + this.addressLines = Collections.unmodifiableList( + normalizeAddressLines(new ArrayList<String>(builder.addressLines))); + this.languageCode = builder.language; + } + + // Helper to normalize a list of address lines. The input may contain null entries or strings + // which must be split into multiple lines. The resulting list entries will be trimmed + // consistently with String.trim() and any empty results are ignored. + // TODO: Trim entries properly with respect to Unicode whitespace. + private static List<String> normalizeAddressLines(List<String> lines) { + // Guava equivalent code for each line would look something like: + // Splitter.on("\n").trimResults(CharMatcher.inRange('\0', ' ')).omitEmptyStrings(); + for (int index = 0; index < lines.size(); ) { + String line = lines.remove(index); + if (line == null) { + continue; + } + if (line.contains("\n")) { + for (String splitLine : line.split("\n")) { + index = maybeAddTrimmedLine(splitLine, lines, index); + } + } else { + index = maybeAddTrimmedLine(line, lines, index); + } + } + return lines; + } + + // Helper to trim a string and (if not empty) add it to the given list at the specified index. + // Returns the new index at which any following elements should be added. + private static int maybeAddTrimmedLine(String line, List<String> lines, int index) { + line = Util.trimToNull(line); + if (line != null) { + lines.add(index++, line); + } + return index; + } + + /** + * Returns a string representation of the address, used for debugging. + */ + @Override + public String toString() { + StringBuilder output = new StringBuilder("(AddressData: " + + "POSTAL_COUNTRY=" + postalCountry + "; " + + "LANGUAGE=" + languageCode + "; "); + for (String line : addressLines) { + output.append(line + "; "); + } + output.append("ADMIN_AREA=" + administrativeArea + "; " + + "LOCALITY=" + locality + "; " + + "DEPENDENT_LOCALITY=" + dependentLocality + "; " + + "POSTAL_CODE=" + postalCode + "; " + + "SORTING_CODE=" + sortingCode + "; " + + "ORGANIZATION=" + organization + "; " + + "RECIPIENT=" + recipient + + ")"); + return output.toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof AddressData)) { + return false; + } + AddressData addressData = (AddressData) o; + + return (postalCountry == null + ? addressData.getPostalCountry() == null + : postalCountry.equals(addressData.getPostalCountry())) + && (addressLines == null + ? addressData.getAddressLines() == null + : addressLines.equals(addressData.getAddressLines())) + && (administrativeArea == null + ? addressData.getAdministrativeArea() == null + : this.getAdministrativeArea().equals(addressData.getAdministrativeArea())) + && (locality == null + ? addressData.getLocality() == null + : locality.equals(addressData.getLocality())) + && (dependentLocality == null + ? addressData.getDependentLocality() == null + : dependentLocality.equals(addressData.getDependentLocality())) + && (postalCode == null + ? addressData.getPostalCode() == null + : postalCode.equals(addressData.getPostalCode())) + && (sortingCode == null + ? addressData.getSortingCode() == null + : sortingCode.equals(addressData.getSortingCode())) + && (organization == null + ? addressData.getOrganization() == null + : organization.equals(addressData.getOrganization())) + && (recipient == null + ? addressData.getRecipient() == null + : recipient.equals(addressData.getRecipient())) + && (languageCode == null + ? this.getLanguageCode() == null + : languageCode.equals(addressData.getLanguageCode())); + } + + @Override + public int hashCode() { + // 17 and 31 are arbitrary seed values. + int result = 17; + + String[] fields = + new String[] { + postalCountry, + administrativeArea, + locality, + dependentLocality, + postalCode, + sortingCode, + organization, + recipient, + languageCode + }; + + for (String field : fields) { + result = 31 * result + (field == null ? 0 : field.hashCode()); + } + + // The only significant field which is not a String. + result = 31 * result + (addressLines == null ? 0 : addressLines.hashCode()); + + return result; + } + + /** + * Returns the CLDR region code for this address; note that this is <em>not</em> the same as the + * ISO 3166-1 2-letter country code. While technically optional, this field will always be set + * by the address widget when an address is entered or edited, and will be assumed to be set by + * many other tools. + * <p> + * While they have most of their values in common with the CLDR region codes, the ISO 2-letter + * country codes have one significant disadvantage; they are not stable and values can change over + * time. For example {@code "CS"} was originally used to represent Czechoslovakia, but later + * represented Serbia and Montenegro. In contrast, CLDR region codes are never reused and can + * represent more regions, such as Ascension Island (AC). + * <p> + * See the page on + * <a href="http://unicode.org/cldr/charts/26/supplemental/territory_containment_un_m_49.html"> + * Territory Containment</a> for a list of CLDR region codes. + * <p> + * Note that the region codes not user-presentable; "GB" is Great Britain but this should always + * be displayed to a user as "UK" or "United Kingdom". + */ + public String getPostalCountry() { + return postalCountry; + } + + /** + * Returns multiple free-form address lines representing low level parts of an address, + * possibly empty. The first line represents the lowest level part of the address, other than + * recipient or organization. + * <p> + * Note that the number of lines returned by this method may be greater than the number of lines + * set on the original builder if some of the lines contained embedded newlines. The values + * returned by this method will never contain embedded newlines. + * <p> + * For example: + * <pre>{@code + * data = AddressData.builder() + * .setAddressLine1("First line\nSecond line") + * .setAddressLine2("Last line") + * .build(); + * // We end up with 3 lines in the final AddressData instance: + * // data.getAddressLines() == [ "First line", "Second line", "Last line" ] + * }</pre> + */ + public List<String> getAddressLines() { + return addressLines; + } + + /** @deprecated Use {@link #getAddressLines} in preference. */ + @Deprecated + public String getAddressLine1() { + return getAddressLine(1); + } + + /** @deprecated Use {@link #getAddressLines} in preference. */ + @Deprecated + public String getAddressLine2() { + return getAddressLine(2); + } + + // Helper for returning the Nth address line. This is split out here so that it's easily to + // change the maximum number of address lines we support. + private String getAddressLine(int lineNumber) { + // If not the last available line, OR if we're the last line but there are no extra lines... + if (lineNumber < ADDRESS_LINE_COUNT || lineNumber >= addressLines.size()) { + return (lineNumber <= addressLines.size()) ? addressLines.get(lineNumber - 1) : null; + } + // We're asking for the last available line and there are additional lines in the data. + // Here it should be true that: lineNumber == ADDRESS_LINE_COUNT + // Guava equivalent: + // return Joiner.on(", ") + // .join(addressLines.subList(ADDRESS_LINE_COUNT - 1, addressLines.size())); + StringBuilder joinedLastLine = new StringBuilder(addressLines.get(lineNumber - 1)); + for (String line : addressLines.subList(lineNumber, addressLines.size())) { + joinedLastLine.append(", ").append(line); + } + return joinedLastLine.toString(); + } + + /** + * Returns the top-level administrative subdivision of this country. Different postal countries + * use different names to refer to their administrative areas. For example: "state" (US), "region" + * (Italy) or "prefecture" (Japan). + * <p> + * Where data is available, the user will be able to select the administrative area name from a + * drop-down list, ensuring that it has only expected values. However this is not always possible + * and no strong assumptions about validity should be made by the user for this value. + */ + public String getAdministrativeArea() { + return administrativeArea; + } + + /** + * Returns the language specific locality, if present. The usage of this field varies by region, + * but it generally refers to the "city" or "town" of the address. Some regions do not use this + * field; their address lines combined with things like postal code or administrative area are + * sufficient to locate an address. + * <p> + * Different countries use different names to refer to their localities. For example: "city" (US), + * "comune" (Italy) or "post town" (Great Britain). For Japan this would return the shikuchouson + * and sub-shikuchouson. + */ + public String getLocality() { + return locality; + } + + /** + * Returns the dependent locality, if present. + * <p> + * This is used for neighborhoods and suburbs. Typically a dependent locality will represent a + * smaller geographical area than a locality, but need not be contained within it. + */ + public String getDependentLocality() { + return dependentLocality; + } + + /** + * Returns the postal code of the address, if present. This value is not language specific but + * may contain arbitrary formatting characters such as spaces or hyphens and might require + * normalization before any meaningful comparison of values. + * <p> + * For example: "94043", "94043-1351", "SW1W", "SW1W 9TQ". + */ + public String getPostalCode() { + return postalCode; + } + + /** + * Returns the sorting code if present. Sorting codes are distinct from postal codes and only + * used in a handful of regions (eg, France). + * <p> + * For example in France this field would contain a + * <a href="http://en.wikipedia.org/wiki/List_of_postal_codes_in_France">CEDEX</a> value. + */ + public String getSortingCode() { + return sortingCode; + } + + /** + * Returns the free form organization string, if present. No assumptions should be made about + * the contents of this field. This field exists because in some situations the organization + * and recipient fields must be treated specially during formatting. It is not a good idea to + * allow users to enter the organization or recipient in the street address lines as this will + * result in badly formatted and non-geocodeable addresses. + */ + public String getOrganization() { + return organization; + } + + /** + * Returns the free form recipient string, if present. No assumptions should be made about the + * contents of this field. This field exists because in some situations the organization + * and recipient fields must be treated specially during formatting. It is not a good idea to + * allow users to enter the organization or recipient in the street address lines as this will + * result in badly formatted and non-geocodeable addresses. + */ + public String getRecipient() { + return recipient; + } + + /** + * Returns a value for those address fields which map to a single string value. + * <p> + * Note that while it is possible to pass {@link AddressField#ADDRESS_LINE_1} and + * {@link AddressField#ADDRESS_LINE_2} into this method, these fields are deprecated and will be + * removed. In general you should be using named methods to obtain specific values for the address + * (eg, {@link #getAddressLines()}) and avoid iterating in a general way over the fields. + * This method has very little value outside of the widget itself and is scheduled for removal. + * + * @deprecated Do not use; scheduled for removal from the public API. + */ + @Deprecated + @SuppressWarnings("deprecation") + // TODO: Move this to a utility method rather than exposing it in the public API. + public String getFieldValue(AddressField field) { + switch (field) { + case COUNTRY: + return postalCountry; + case ADMIN_AREA: + return administrativeArea; + case LOCALITY: + return locality; + case DEPENDENT_LOCALITY: + return dependentLocality; + case POSTAL_CODE: + return postalCode; + case SORTING_CODE: + return sortingCode; + case ADDRESS_LINE_1: + return getAddressLine1(); + case ADDRESS_LINE_2: + return getAddressLine2(); + case ORGANIZATION: + return organization; + case RECIPIENT: + return recipient; + default: + throw new IllegalArgumentException("multi-value fields not supported: " + field); + } + } + + /** + * Returns the BCP-47 language code for this address which defines the language we expect to be + * used for any language specific fields. If this method returns {@code null} then the language + * is assumed to be in the default (most used) language for the region code of the address; + * although the precise determination of a default language is often approximate and may change + * over time. Wherever possible it is recommended to construct {@code AddressData} instances + * with a specific language code. + * <p> + * Languages are used to guide how the address is <a + * href="http://en.wikipedia.org/wiki/Mailing_address_format_by_country"> formatted for + * display</a>. The same address may have different {@link AddressData} representations in + * different languages. For example, the French name of "New Mexico" is "Nouveau-Mexique". + */ + public String getLanguageCode() { + return languageCode; + } + + /** Returns a new builder to construct an {@code AddressData} instance. */ + public static Builder builder() { + return new Builder(); + } + + /** Returns a new builder to construct an {@code AddressData} instance. */ + public static Builder builder(AddressData address) { + return builder().set(address); + } + + /** Builder for AddressData. */ + public static final class Builder { + // A map of single value address fields to their values. + private final Map<AddressField, String> fields = new HashMap<AddressField, String>(); + // The address lines, not normalized. + private final List<String> addressLines = new ArrayList<String>(); + // The BCP-47 language of the address. + private String language = null; + + /** + * Constructs an empty builder for AddressData instances. Prefer to use one of the + * {@link AddressData#builder} methods in preference to this. + */ + // TODO: Migrate users and make this private. + public Builder() {} + + /** + * Constructs a builder for AddressData instances using data from the given address. + * Prefer to use one of the {@link AddressData#builder} methods in preference to this. + * + * @deprecated Use the builder() methods on AddressData in preference to this. + */ + @Deprecated + // TODO: Migrate users and delete this method. + public Builder(AddressData address) { + set(address); + } + + /** + * Sets the 2-letter CLDR region code of the address; see + * {@link AddressData#getPostalCountry()}. Unlike other values passed to the builder, the + * region code can never be null. + * + * @param regionCode the CLDR region code of the address. + */ + // TODO: Rename to setRegionCode. + public Builder setCountry(String regionCode) { + return set(AddressField.COUNTRY, checkNotNull(regionCode)); + } + + /** + * Sets or clears the administrative area of the address; see + * {@link AddressData#getAdministrativeArea()}. + * + * @param adminAreaName the administrative area name, or null to clear an existing value. + */ + // TODO: Rename to setAdministrativeArea. + public Builder setAdminArea(String adminAreaName) { + return set(AddressField.ADMIN_AREA, adminAreaName); + } + + /** + * Sets or clears the locality of the address; see {@link AddressData#getLocality()}. + * + * @param locality the locality name, or null to clear an existing value. + */ + public Builder setLocality(String locality) { + return set(AddressField.LOCALITY, locality); + } + + /** + * Sets or clears the dependent locality of the address; see + * {@link AddressData#getDependentLocality()}. + * + * @param dependentLocality the dependent locality name, or null to clear an existing value. + */ + public Builder setDependentLocality(String dependentLocality) { + return set(AddressField.DEPENDENT_LOCALITY, dependentLocality); + } + + /** + * Sets or clears the postal code of the address; see {@link AddressData#getPostalCode()}. + * + * @param postalCode the postal code, or null to clear an existing value. + */ + public Builder setPostalCode(String postalCode) { + return set(AddressField.POSTAL_CODE, postalCode); + } + + /** + * Sets or clears the sorting code of the address; see {@link AddressData#getSortingCode()}. + * + * @param sortingCode the sorting code, or null to clear an existing value. + */ + public Builder setSortingCode(String sortingCode) { + return set(AddressField.SORTING_CODE, sortingCode); + } + + /** + * Sets or clears the BCP-47 language code for this address (eg, "en" or "zh-Hant"). If the + * language is not set, then the address will be assumed to be in the default language of the + * country of the address; however it is highly discouraged to rely on this as the default + * language may change over time. See {@link AddressData#getLanguageCode()}. + * + * @param languageCode the BCP-47 language code, or null to clear an existing value. + */ + public Builder setLanguageCode(String languageCode) { + this.language = languageCode; + return this; + } + + /** + * Sets multiple unstructured street level lines in the address. Calling this method will + * always discard any existing address lines before adding new ones. + * <p> + * Note that the number of lines set by this method is preserved in the builder's state but a + * single line set here may result in multiple lines in the resulting {@code AddressData} + * instance if it contains embedded newline characters. + * <p> + * For example: + * <pre>{@code + * data = AddressData.builder() + * .setAddressLines(Arrays.asList("First line\nSecond line")) + * .setAddressLine2("Last line"); + * .build(); + * // data.getAddressLines() == [ "First line", "Second line", "Last line" ] + * }</pre> + */ + public Builder setAddressLines(Iterable<String> lines) { + addressLines.clear(); + for (String line : lines) { + addressLines.add(line); + } + return this; + } + + /** + * Adds another address line. Embedded newlines will be normalized when "build()" is called. + */ + // TODO: Consider removing this method if nobody is using it to simplify the API. + public Builder addAddressLine(String value) { + addressLines.add(value); + return this; + } + + /** + * Sets multiple street lines from a single street string, clearing any existing address lines + * first. The input string may contain new lines which will result in multiple separate lines + * in the resulting {@code AddressData} instance. After splitting, each line is trimmed and + * empty lines are ignored. + * <p> + * Example: {@code " \n \n1600 Amphitheatre Ave\n\nRoom 122"} will set the lines: + * <ol> + * <li>"1600 Amphitheatre Ave" + * <li>"Room 122" + * </ol> + * + * @param value a string containing one or more address lines, separated by {@code "\n"}. + */ + public Builder setAddress(String value) { + addressLines.clear(); + addressLines.add(value); + normalizeAddressLines(addressLines); + return this; + } + + /** + * Copies all the data of the given address into the builder. Any existing data in the builder + * is discarded. + */ + public Builder set(AddressData data) { + fields.clear(); + for (AddressField addressField : SINGLE_VALUE_FIELDS) { + set(addressField, data.getFieldValue(addressField)); + } + addressLines.clear(); + addressLines.addAll(data.addressLines); + setLanguageCode(data.getLanguageCode()); + return this; + } + + /** + * TODO: Remove this method in favor of setAddressLines(Iterable<String>). + * + * @deprecated Use {@link #setAddressLines} instead. + */ + @Deprecated + public Builder setAddressLine1(String value) { + return setAddressLine(1, value); + } + + /** + * TODO: Remove this method in favor of setAddressLines(Iterable<String>). + * + * @deprecated Use {@link #setAddressLines} instead. + */ + @Deprecated + public Builder setAddressLine2(String value) { + return setAddressLine(2, value); + } + + /** + * Sets or clears the organization of the address; see {@link AddressData#getOrganization()}. + * + * @param organization the organization, or null to clear an existing value. + */ + public Builder setOrganization(String organization) { + return set(AddressField.ORGANIZATION, organization); + } + + /** + * Sets or clears the recipient of the address; see {@link AddressData#getRecipient()}. + * + * @param recipient the recipient, or null to clear an existing value. + */ + public Builder setRecipient(String recipient) { + return set(AddressField.RECIPIENT, recipient); + } + + /** + * Sets an address field with the specified value. If the value is empty (null or whitespace), + * the original value associated with the field will be removed. + * + * @deprecated Do not use; scheduled for removal from the public API. + */ + @Deprecated + @SuppressWarnings("deprecation") + // TODO: Reimplement using public API as a utility function in AddressWidget (the only caller). + public Builder set(AddressField field, String value) { + if (SINGLE_VALUE_FIELDS.contains(field)) { + value = Util.trimToNull(value); + if (value == null) { + fields.remove(field); + } else { + fields.put(field, value); + } + } else if (field == AddressField.STREET_ADDRESS) { + if (value == null) { + addressLines.clear(); + } else { + setAddress(value); + } + } else { + int lineNum = ADDRESS_LINE_FIELDS.indexOf(field) + 1; + if (lineNum > 0) { + setAddressLine(lineNum, value); + } + } + return this; + } + + // This may preserve whitespace at the ends of lines, but this gets normalized when we build + // the data instance. + private Builder setAddressLine(int lineNum, String value) { + if (Util.trimToNull(value) == null) { + if (lineNum < addressLines.size()) { + // Clearing an element that isn't the last in the list. + addressLines.set(lineNum - 1, null); + } else if (lineNum == addressLines.size()) { + // Clearing the last element (remove it and clear up trailing nulls). + addressLines.remove(lineNum - 1); + for (int i = addressLines.size() - 1; i >= 0 && addressLines.get(i) == null; i--) { + addressLines.remove(i); + } + } + } else { + // Padding the list with nulls if necessary. + for (int i = addressLines.size(); i < lineNum; i++) { + addressLines.add(null); + } + // Set the non-null value. + addressLines.set(lineNum - 1, value); + } + return this; + } + + /** + * Builds an AddressData instance from the current state of the builder. A builder instance may + * be used to build multiple data instances. + * <p> + * During building the street address line information is normalized and the following will be + * true for any build instance. + * <ol> + * <li>The order of address lines is retained relative to the builder. + * <li>Empty address lines (empty strings, whitespace only or null) are removed. + * <li>Remaining address lines are trimmed of whitespace. + * </ol> + */ + public AddressData build() { + return new AddressData(this); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressDataKey.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressDataKey.java new file mode 100644 index 00000000000..3b8fb49c74f --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressDataKey.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enumerates all the data fields found in the JSON-format address property data that are used by + * the Android Address Input Widget. + */ +public enum AddressDataKey { + /** + * Identifies the countries for which data is provided. + */ + COUNTRIES, + /** + * The standard format string. This identifies which fields can be used in the address, along with + * their order. This also carries additional information for use in formatting the fields into + * multiple lines. This is also used to indicate which fields should _not_ be used for an address. + */ + FMT, + /** + * The unique ID of the region, in the form of a path from parent IDs to the key. + */ + ID, + /** + * The key of the region, unique to its parent. If there is an accepted abbreviation for this + * region, then the key will be set to this and name will be set to the local name for this + * region. If there is no accepted abbreviation, then this key will be the local name and there + * will be no local name specified. This value must be present. + */ + KEY, + /** + * The default language of any data for this region, if known. + */ + LANG, + /** + * The languages used by any data for this region, if known. + */ + LANGUAGES, + /** + * The latin format string {@link #FMT} used when a country defines an alternative format for + * use with the latin script, such as in China. + */ + LFMT, + /** + * Indicates the type of the name used for the locality (city) field. + */ + LOCALITY_NAME_TYPE, + /** + * Indicates which fields must be present in a valid address. + */ + REQUIRE, + /** + * Indicates the type of the name used for the state (administrative area) field. + */ + STATE_NAME_TYPE, + /** + * Indicates the type of the name used for the sublocality field. + */ + SUBLOCALITY_NAME_TYPE, + /** + * Encodes the {@link #KEY} value of all the children of this region. + */ + SUB_KEYS, + /** + * Encodes the transliterated latin name value of all the children of this region, if the local + * names are not in latin script already. + */ + SUB_LNAMES, + /** + * Indicates, for each child of this region, whether that child has additional children. + */ + SUB_MORES, + /** + * Encodes the local name value of all the children of this region. + */ + SUB_NAMES, + /** + * Encodes width overrides for specific fields. + */ + WIDTH_OVERRIDES, + /** + * Encodes the {@link #ZIP} value for the subtree beneath this region. + */ + XZIP, + /** + * Encodes the postal code pattern if at the country level, and the postal code prefix if at a + * level below country. + */ + ZIP, + /** + * Indicates the type of the name used for the ZIP (postal code) field. + */ + ZIP_NAME_TYPE; + + /** + * Returns a field based on its keyname (value in the JSON-format file), or null if no field + * matches. + */ + static AddressDataKey get(String keyname) { + return ADDRESS_KEY_NAME_MAP.get(Util.toLowerCaseLocaleIndependent(keyname)); + } + + private static final Map<String, AddressDataKey> ADDRESS_KEY_NAME_MAP = + new HashMap<String, AddressDataKey>(); + + static { + // Populates the map of enums against their lower-cased string values for easy look-up. + for (AddressDataKey field : values()) { + ADDRESS_KEY_NAME_MAP.put(Util.toLowerCaseLocaleIndependent(field.toString()), field); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressField.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressField.java new file mode 100644 index 00000000000..08e570e119d --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressField.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Identifiers for the input fields of the address widget, used to control options related to + * visibility and ordering of UI elements. Note that one {@code AddressField} may represent more + * than one input field in the UI (eg, {@link #STREET_ADDRESS}), but each input field can be + * identified by exactly one {@code AddressField}. + * <p> + * In certain use cases not all fields are necessary, and you can hide fields using + * {@link FormOptions#setHidden}. An example of this is when you are collecting postal addresses not + * intended for delivery and wish to suppress the collection of a recipient's name or organization. + * <p> + * An alternative to hiding fields is to make them read-only, using {@link FormOptions#setReadonly}. + * An example of this would be in the case that the country of an address was already determined but + * we wish to make it clear to the user that we have already taken it into account and do not want + * it entered again. + * + * @see FormOptions + */ +public enum AddressField { + /** The drop-down menu used to select a region for {@link AddressData#getPostalCountry()}. */ + COUNTRY('R'), + /** + * The input field used to enter a value for {@link AddressData#getAddressLine1()}. + * @deprecated Use {@link #STREET_ADDRESS} instead. + */ + @Deprecated + ADDRESS_LINE_1('1'), + /** + * The input field used to enter a value for {@link AddressData#getAddressLine2()}. + * @deprecated Use {@link #STREET_ADDRESS} instead. + */ + @Deprecated + ADDRESS_LINE_2('2'), + /** The input field(s) used to enter values for {@link AddressData#getAddressLines()}. */ + STREET_ADDRESS('A'), + /** The input field used to enter a value for {@link AddressData#getAdministrativeArea()}. */ + ADMIN_AREA('S'), + /** The input field used to enter a value for {@link AddressData#getLocality()}. */ + LOCALITY('C'), + /** The input field used to enter a value for {@link AddressData#getDependentLocality()}. */ + DEPENDENT_LOCALITY('D'), + /** The input field used to enter a value for {@link AddressData#getPostalCode()}. */ + POSTAL_CODE('Z'), + /** The input field used to enter a value for {@link AddressData#getSortingCode()}. */ + SORTING_CODE('X'), + + /** The input field used to enter a value for {@link AddressData#getRecipient()}. */ + RECIPIENT('N'), + /** The input field used to enter a value for {@link AddressData#getOrganization()}. */ + ORGANIZATION('O'); + + /** Classification of the visual width of address input fields. */ + public enum WidthType { + /** + * Identifies an input field as accepting full-width input, such as address lines or recipient. + */ + LONG, + /** + * Identifies an input field as accepting short (often bounded) input, such as postal code. + */ + SHORT; + + static WidthType of(char c) { + switch (c) { + // In case we need a 'narrow'. Map it to 'S' for now to facilitate the rollout. + case 'N': + case 'S': + return SHORT; + case 'L': + return LONG; + default: + throw new IllegalArgumentException("invalid width character: " + c); + } + } + } + + private static final Map<Character, AddressField> FIELD_MAPPING; + + static { + Map<Character, AddressField> map = new HashMap<Character, AddressField>(); + for (AddressField value : values()) { + map.put(value.getChar(), value); + } + FIELD_MAPPING = Collections.unmodifiableMap(map); + } + + // Defines the character codes used in the metadata to specify the types of fields used in + // address formatting. Note that the metadata also has a character for newlines, which is + // not defined here. + private final char idChar; + + private AddressField(char c) { + this.idChar = c; + } + + /** + * Returns the AddressField corresponding to the given identification character. + * + * @throws IllegalArgumentException if the identifier does not correspond to a valid field. + */ + static AddressField of(char c) { + AddressField field = FIELD_MAPPING.get(c); + if (field == null) { + throw new IllegalArgumentException("invalid field character: " + c); + } + return field; + } + + /** + * Returns the field's identification character, as used in the metadata. + * + * @return identification char. + */ + char getChar() { + return idChar; + } + + /** + * Returns default width of this address field. This may be overridden for a specific country when + * we have data for the possible inputs in that field and use a drop-down, rather than a text + * field, in the UI. + */ + // TODO: We'd probably be better off just having a widthType field in the enum. + private WidthType getDefaultWidthType() { + return this.equals(POSTAL_CODE) || this.equals(SORTING_CODE) ? WidthType.SHORT : WidthType.LONG; + } + + /** + * Returns default width of this address field. Takes per-country heuristics into account for + * text input fields. This may be overridden for a specific country when we have data for the + * possible inputs in that field and use a drop-down, rather than a text field, in the UI. + */ + public WidthType getWidthTypeForRegion(String regionCode) { + Util.checkNotNull(regionCode); + WidthType width = FormatInterpreter.getWidthOverride(this, regionCode); + return width != null ? width : getDefaultWidthType(); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblemType.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblemType.java new file mode 100644 index 00000000000..5afdbe93126 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblemType.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * Enumerates problems that default address verification can report. + */ +// This is an external class and part of the widget's public API. +// TODO: Review public API for external classes. +public enum AddressProblemType { + /** + * The field is not null and not whitespace, and the field should not be used by addresses in this + * country. + * <p> + * For example, in the U.S. the SORTING_CODE field is unused, so its presence is an + * error. This cannot happen when using the Address Widget to enter an address. + */ + UNEXPECTED_FIELD, + + /** + * The field is null or whitespace, and the field is required for addresses in this country. + * <p> + * For example, in the U.S. ADMIN_AREA is a required field. + */ + MISSING_REQUIRED_FIELD, + + /** + * A list of values for the field is defined and the value does not occur in the list. Applies + * to hierarchical elements like REGION, ADMIN_AREA, LOCALITY, and DEPENDENT_LOCALITY. + * + * <p>For example, in the U.S. the only valid values for ADMIN_AREA are the two-letter state + * codes. + */ + UNKNOWN_VALUE, + + /** + * A format for the field is defined and the value does not match. This is used to match + * POSTAL_CODE against the general format pattern. Formats indicate how many digits/letters should + * be present, and what punctuation is allowed. + * <p> + * For example, in the U.S. postal codes are five digits with an optional hyphen followed by + * four digits. + */ + INVALID_FORMAT, + + /** + * A specific pattern for the field is defined and the value does not match. This is used to match + * example) and the value does not match. This is used to match POSTAL_CODE against a regular + * expression. + * <p> + * For example, in the US postal codes in the state of California start with a '9'. + */ + MISMATCHING_VALUE; + + /** + * Returns a unique string identifying this problem (for use in a message catalog). + */ + public String keyname() { + return Util.toLowerCaseLocaleIndependent(name()); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java new file mode 100644 index 00000000000..906d56aa88d --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * This structure keeps track of any errors found when validating the AddressData. + */ +// This is an external class and part of the widget's public API. +// TODO: Review public API for external classes and tidy JavaDoc. +public final class AddressProblems { + private Map<AddressField, AddressProblemType> problems = + new HashMap<AddressField, AddressProblemType>(); + + /** + * Adds a problem of the given type for the given address field. Only one address problem is + * saved per address field. + */ + void add(AddressField addressField, AddressProblemType problem) { + problems.put(addressField, problem); + } + + /** + * Returns true if no problems have been added. + */ + public boolean isEmpty() { + return problems.isEmpty(); + } + + @Override + public String toString() { + return problems.toString(); + } + + public void clear() { + problems.clear(); + } + + /** + * Returns null if no problems exists. + */ + public AddressProblemType getProblem(AddressField addressField) { + return problems.get(addressField); + } + + /** + * This will return an empty map if there are no problems. + */ + public Map<AddressField, AddressProblemType> getProblems() { + return problems; + } + + /** + * Adds all problems this object contains to the given {@link AddressProblems} object. + */ + public void copyInto(AddressProblems other) { + for (Entry<AddressField, AddressProblemType> problem : problems.entrySet()) { + other.add(problem.getKey(), problem.getValue()); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationData.java new file mode 100644 index 00000000000..35a1e7844c0 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationData.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Wraps a Map of address property data to provide the AddressVerificationData API. + */ +public final class AddressVerificationData implements DataSource { + private final Map<String, String> propertiesMap; + + private static final Pattern KEY_VALUES_PATTERN = Pattern.compile("\"([^\"]+)\":\"([^\"]*)\""); + + private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\",\""); + + /** + * Constructs from a map of address property data. This keeps a reference to the map. This + * does not mutate the map. The map should not be mutated subsequent to this call. + */ + public AddressVerificationData(Map<String, String> propertiesMap) { + this.propertiesMap = propertiesMap; + } + + /** + * This will not return null. + */ + @Override + public AddressVerificationNodeData getDefaultData(String key) { + // gets country key + if (key.split("/").length > 1) { + String[] parts = key.split("/"); + key = parts[0] + "/" + parts[1]; + } + + AddressVerificationNodeData data = get(key); + if (data == null) { + throw new RuntimeException("failed to get default data with key " + key); + } + return data; + } + + @Override + public AddressVerificationNodeData get(String key) { + String json = propertiesMap.get(key); + if (json != null && isValidDataKey(key)) { + return createNodeData(json); + } + return null; + } + + /** + * Returns a set of the keys for which verification data is provided. The returned set is + * immutable. + */ + Set<String> keys() { + Set<String> result = new HashSet<String>(); + for (String key : propertiesMap.keySet()) { + if (isValidDataKey(key)) { + result.add(key); + } + } + return Collections.unmodifiableSet(result); + } + + /** + * Returns whether the key is a "data" key rather than an "examples" key. + */ + private boolean isValidDataKey(String key) { + return key.startsWith("data"); + } + + /** + * Returns the contents of the JSON-format string as a map. + */ + AddressVerificationNodeData createNodeData(String json) { + // Remove leading and trailing { and }. + json = json.substring(1, json.length() - 1); + Map<AddressDataKey, String> map = new EnumMap<AddressDataKey, String>(AddressDataKey.class); + + // our objects are very simple so we parse manually + // - no double quotes within strings + // - no extra spaces + // can't use split "," since some data has commas in it. + Matcher sm = SEPARATOR_PATTERN.matcher(json); + int pos = 0; + while (pos < json.length()) { + String pair; + if (sm.find()) { + pair = json.substring(pos, sm.start() + 1); + pos = sm.start() + 2; + } else { + pair = json.substring(pos); + pos = json.length(); + } + + Matcher m = KEY_VALUES_PATTERN.matcher(pair); + if (m.matches()) { + String value = m.group(2); + + // Remove escaped backslashes. + // Java regex doesn't handle a replacement String consisting of + // a single backslash, and treats a replacement String consisting of + // two backslashes as two backslashes instead of one. So there's + // no way to use regex to replace a match with a single backslash, + // apparently. + if (value.length() > 0) { + char[] linechars = m.group(2).toCharArray(); + int w = 1; + for (int r = w; r < linechars.length; ++r) { + char c = linechars[r]; + if (c == '\\' && linechars[w - 1] == '\\') { + // don't increment w; + continue; + } + linechars[w++] = c; + } + value = new String(linechars, 0, w); + } + + AddressDataKey df = AddressDataKey.get(m.group(1)); + if (df == null) { + // Skip this data - it isn't used in the Android version. + } else { + map.put(df, value); + } + } else { + // This is a runtime data sanity check. The data should be + // checked when the data is built. The JSON data string should + // be parsable into string pairs using SEP_PAT. + throw new RuntimeException("could not match '" + pair + "' in '" + json + "'"); + } + } + + return new AddressVerificationNodeData(map); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationNodeData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationNodeData.java new file mode 100644 index 00000000000..125120b7fc2 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AddressVerificationNodeData.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.Map; + +/** + * A map of {@link AddressDataKey}s to JSON strings. Provides data for a single node in the address + * data hierarchy (for example, "data/US/CA"). Key is an AddressDataKey and the value is the raw + * string representing that data. This is either a single string, or an array of strings represented + * as a single string using '~' to separate the elements of the array, depending on the + * AddressDataKey. + */ +public final class AddressVerificationNodeData { + private final Map<AddressDataKey, String> map; + + public AddressVerificationNodeData(Map<AddressDataKey, String> map) { + Util.checkNotNull(map, "Cannot construct StandardNodeData with null map"); + this.map = map; + } + + public boolean containsKey(AddressDataKey key) { + return map.containsKey(key); + } + + /** + * Gets the value for a particular key in the map. + */ + public String get(AddressDataKey key) { + return map.get(key); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AsyncRequestApi.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AsyncRequestApi.java new file mode 100644 index 00000000000..cf5f74aeca1 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/AsyncRequestApi.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * Abstracted low-level network request API. Implementations making real network requests should + * attempt to redirect and retry as necessary such that any failure can be considered definitive, as + * the users of this interface will never retry failed requests. + */ +public interface AsyncRequestApi { + /** + * Requests JSON metadata from the given URL and invokes the appropriate methods in the given + * callback. If the given callback is null, the asynchronous request is still made but no callback + * methods are invoked. If the given timeout is exceeded then the implementation should, where + * feasible, attempt to cancel the in-progress network request, but must always invoke the + * {@link AsyncCallback#onFailure()} callback a short, bounded time after the timeout occurred. + * + * @param url the complete URL for the request + * @param callback the optional callback to be invoked when the request is complete + * @param timeoutMillis the timeout for the request in milliseconds + */ + void requestObject(String url, AsyncCallback callback, int timeoutMillis); + + /** + * Callback API for network requests. One of the methods in this API will be invoked by the + * {@link AsyncRequestApi} implementation once the network request is complete or has timed out. + */ + public interface AsyncCallback { + /** Invoked with the parsed JsoMap from the successful request. */ + public void onSuccess(JsoMap result); + + /** Invoked when a request has definitely failed (after possible retries, redirection ...). */ + public void onFailure(); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/CacheData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/CacheData.java new file mode 100644 index 00000000000..12c36b6de3d --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/CacheData.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import static com.google.i18n.addressinput.common.Util.checkNotNull; + +import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.logging.Logger; + +/** + * Cache for dynamic address data. + */ +public final class CacheData { + private static final Logger logger = Logger.getLogger(CacheData.class.getName()); + + /** + * Time-out value for the server to respond in milliseconds. + */ + private static final int TIMEOUT = 5000; + + /** + * URL to get address data. You can also reset it by calling {@link #setUrl(String)}. + */ + private String serviceUrl; + + private final AsyncRequestApi asyncRequestApi; + + /** + * Storage for all dynamically retrieved data. + */ + private final JsoMap cache; + + /** + * CacheManager that handles caching that is needed by the client of the Address Widget. + */ + private final ClientCacheManager clientCacheManager; + + /** + * All requests that have been sent. + */ + private final HashSet<String> requestedKeys = new HashSet<String>(); + + /** + * All invalid requested keys. For example, if we request a random string "asdfsdf9o", and the + * server responds by saying this key is invalid, it will be stored here. + */ + private final HashSet<String> badKeys = new HashSet<String>(); + + /** + * Temporary store for {@code CacheListener}s. When a key is requested and still waiting for + * server's response, the listeners for the same key will be temporary stored here. When the + * server responded, these listeners will be triggered and then removed. + */ + private final HashMap<LookupKey, HashSet<CacheListener>> temporaryListenerStore = + new HashMap<LookupKey, HashSet<CacheListener>>(); + + /** + * Creates an instance of CacheData with an empty cache, and uses no caching that is external + * to the AddressWidget. + */ + // TODO(dbeaumont): Remove this method (avoid needless dependency on SimpleClientCacheManager). + public CacheData(AsyncRequestApi asyncRequestApi) { + this(new SimpleClientCacheManager(), asyncRequestApi); + } + + /** + * Creates an instance of CacheData with an empty cache, and uses additional caching (external + * to the AddressWidget) specified by clientCacheManager. + */ + public CacheData(ClientCacheManager clientCacheManager, AsyncRequestApi asyncRequestApi) { + this.clientCacheManager = clientCacheManager; + setUrl(this.clientCacheManager.getAddressServerUrl()); + this.cache = JsoMap.createEmptyJsoMap(); + this.asyncRequestApi = asyncRequestApi; + } + + /** + * This constructor is meant to be used together with external caching. Use case: + * <p> + * After having finished using the address widget: + * <ol> + * <li>String allCachedData = getJsonString(); + * <li>Cache (save) allCachedData wherever makes sense for your service / activity + * </ol> + * <p> + * Before using it next time: + * <ol> + * <li>Get the saved allCachedData string + * <li>new ClientData(new CacheData(allCachedData)) + * </ol> + * <p> + * If you don't have any saved data you can either just pass an empty string to + * this constructor or use the other constructor. + * + * @param jsonString cached data from last time the class was used + */ + // TODO(dbeaumont): Remove this method (if callers need to build from json string, do it first). + public CacheData(String jsonString, AsyncRequestApi asyncRequestApi) { + clientCacheManager = new SimpleClientCacheManager(); + setUrl(clientCacheManager.getAddressServerUrl()); + JsoMap tempMap = null; + try { + tempMap = JsoMap.buildJsoMap(jsonString); + } catch (JSONException jsonE) { + // If parsing the JSON string throws an exception, default to + // starting with an empty cache. + logger.warning("Could not parse json string, creating empty cache instead."); + tempMap = JsoMap.createEmptyJsoMap(); + } finally { + cache = tempMap; + } + this.asyncRequestApi = asyncRequestApi; + } + + /** + * Interface for all listeners to {@link CacheData} change. This is only used when multiple + * requests of the same key is dispatched and server has not responded yet. + */ + private static interface CacheListener extends EventListener { + /** + * The function that will be called when valid data is about to be put in the cache. + * + * @param key the key for newly arrived data. + */ + void onAdd(String key); + } + + /** + * Class to handle JSON response. + */ + private class JsonHandler { + /** + * Key for the requested data. + */ + private final String key; + + /** + * Pre-existing data for the requested key. Null is allowed. + */ + private final JSONObject existingJso; + + private final DataLoadListener listener; + + /** + * Constructs a JsonHandler instance. + * + * @param key The key for requested data. + * @param oldJso Pre-existing data for this key or null. + */ + private JsonHandler(String key, JSONObject oldJso, DataLoadListener listener) { + checkNotNull(key); + this.key = key; + this.existingJso = oldJso; + this.listener = listener; + } + + /** + * Saves valid responded data to the cache once data arrives, or if the key is invalid, + * saves it in the invalid cache. If there is pre-existing data for the key, it will merge + * the new data will the old one. It also triggers {@link DataLoadListener#dataLoadingEnd()} + * method before it returns (even when the key is invalid, or input jso is null). This is + * called from a background thread. + * + * @param map The received JSON data as a map. + */ + private void handleJson(JsoMap map) { + // Can this ever happen? + if (map == null) { + logger.warning("server returns null for key:" + key); + badKeys.add(key); + notifyListenersAfterJobDone(key); + triggerDataLoadingEndIfNotNull(listener); + return; + } + + JSONObject json = map; + String idKey = Util.toLowerCaseLocaleIndependent(AddressDataKey.ID.name()); + if (!json.has(idKey)) { + logger.warning("invalid or empty data returned for key: " + key); + badKeys.add(key); + notifyListenersAfterJobDone(key); + triggerDataLoadingEndIfNotNull(listener); + return; + } + + if (existingJso != null) { + map.mergeData((JsoMap) existingJso); + } + + cache.putObj(key, map); + notifyListenersAfterJobDone(key); + triggerDataLoadingEndIfNotNull(listener); + } + } + + /** + * Sets address data server URL. Input URL cannot be null. + * + * @param url The service URL. + */ + public void setUrl(String url) { + checkNotNull(url, "Cannot set URL of address data server to null."); + serviceUrl = url; + } + + /** + * Gets address data server URL. + */ + public String getUrl() { + return serviceUrl; + } + + /** + * Returns a JSON string representing the data currently stored in this cache. It can be used + * to later create a new CacheData object containing the same cached data. + * + * @return a JSON string representing the data stored in this cache + */ + public String getJsonString() { + return cache.toString(); + } + + /** + * Checks if key and its value is cached (Note that only valid ones are cached). + */ + public boolean containsKey(String key) { + return cache.containsKey(key); + } + + // This method is called from a background thread. + private void triggerDataLoadingEndIfNotNull(DataLoadListener listener) { + if (listener != null) { + listener.dataLoadingEnd(); + } + } + + /** + * Fetches data from server, or returns if the data is already cached. If the fetched data is + * valid, it will be added to the cache. This method also triggers {@link + * DataLoadListener#dataLoadingEnd()} method before it returns. + * + * @param existingJso Pre-existing data for this key or null if none. + * @param listener An optional listener to call when done. + */ + void fetchDynamicData( + final LookupKey key, JSONObject existingJso, final DataLoadListener listener) { + checkNotNull(key, "null key not allowed."); + + notifyStart(listener); + + // Key is valid and cached. + if (cache.containsKey(key.toString())) { + triggerDataLoadingEndIfNotNull(listener); + return; + } + + // Key is invalid and cached. + if (badKeys.contains(key.toString())) { + triggerDataLoadingEndIfNotNull(listener); + return; + } + + // Already requested the key, and is still waiting for server's response. + if (!requestedKeys.add(key.toString())) { + logger.fine("data for key " + key + " requested but not cached yet"); + addListenerToTempStore(key, new CacheListener() { + @Override + public void onAdd(String myKey) { + triggerDataLoadingEndIfNotNull(listener); + } + }); + return; + } + + // Key is in the cache maintained by the client of the AddressWidget. + String dataFromClientCache = clientCacheManager.get(key.toString()); + if (dataFromClientCache != null && dataFromClientCache.length() > 0) { + final JsonHandler handler = new JsonHandler(key.toString(), existingJso, listener); + try { + handler.handleJson(JsoMap.buildJsoMap(dataFromClientCache)); + return; + } catch (JSONException e) { + logger.warning("Data from client's cache is in the wrong format: " + dataFromClientCache); + } + } + + // Key is not cached yet, now sending the request to the server. + final JsonHandler handler = new JsonHandler(key.toString(), existingJso, listener); + asyncRequestApi.requestObject(serviceUrl + "/" + key.toString(), new AsyncCallback() { + @Override + public void onFailure() { + logger.warning("Request for key " + key + " failed"); + requestedKeys.remove(key.toString()); + notifyListenersAfterJobDone(key.toString()); + triggerDataLoadingEndIfNotNull(listener); + } + + @Override + public void onSuccess(JsoMap result) { + handler.handleJson(result); + // Put metadata into the cache maintained by the client of the AddressWidget. + String dataRetrieved = result.toString(); + clientCacheManager.put(key.toString(), dataRetrieved); + } + }, + TIMEOUT); + } + + /** + * Gets region data from our compiled-in java file and stores it in the cache. This is only called + * when data cannot be obtained from the server, so there will be no pre-existing data for this + * key. + */ + void getFromRegionDataConstants(final LookupKey key) { + checkNotNull(key, "null key not allowed."); + String data = RegionDataConstants.getCountryFormatMap().get( + key.getValueForUpperLevelField(AddressField.COUNTRY)); + if (data != null) { + try { + cache.putObj(key.toString(), JsoMap.buildJsoMap(data)); + } catch (JSONException e) { + logger.warning("Failed to parse data for key " + key + " from RegionDataConstants"); + } + } + } + + /** + * Retrieves string data identified by key. + * + * @param key Non-null key. E.g., "data/US/CA". + * @return String value for specified key. + */ + public String get(String key) { + checkNotNull(key, "null key not allowed"); + return cache.get(key); + } + + /** + * Retrieves JsoMap data identified by key. + * + * @param key Non-null key. E.g., "data/US/CA". + * @return String value for specified key. + */ + public JsoMap getObj(String key) { + checkNotNull(key, "null key not allowed"); + return cache.getObj(key); + } + + /** Notifies the listener when we start loading data. */ + private void notifyStart(DataLoadListener listener) { + if (listener != null) { + listener.dataLoadingBegin(); + } + } + + private void notifyListenersAfterJobDone(String key) { + LookupKey lookupKey = new LookupKey.Builder(key).build(); + HashSet<CacheListener> listeners = temporaryListenerStore.get(lookupKey); + if (listeners != null) { + for (CacheListener listener : listeners) { + listener.onAdd(key.toString()); + } + listeners.clear(); + } + } + + private void addListenerToTempStore(LookupKey key, CacheListener listener) { + checkNotNull(key); + checkNotNull(listener); + HashSet<CacheListener> listeners = temporaryListenerStore.get(key); + if (listeners == null) { + listeners = new HashSet<CacheListener>(); + temporaryListenerStore.put(key, listeners); + } + listeners.add(listener); + } + + /** + * Added for testing purposes. Adds a new object into the cache. + * + * @param id string of the format "data/country/.." ie. "data/US/CA" + * @param object The JSONObject to be put into cache. + */ + void addToJsoMap(String id, JSONObject object) { + cache.putObj(id, object); + } + + /** + * Added for testing purposes. Checks to see if the cache is empty, + * + * @return true if the internal cache is empty + */ + boolean isEmpty() { + return cache.length() == 0; + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientCacheManager.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientCacheManager.java new file mode 100644 index 00000000000..22769864fcc --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientCacheManager.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * Used by AddressWidget to handle caching in client-specific ways. + */ +// This is an external class and part of the widget's public API. +// TODO: Review public API for external classes and tidy JavaDoc. +public interface ClientCacheManager { + /** Get the data that is cached for the given key. */ + public String get(String key); + /** Put the data for the given key into the cache. */ + public void put(String key, String data); + /** Get the URL of the server that serves address metadata. */ + public String getAddressServerUrl(); +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientData.java new file mode 100644 index 00000000000..2b0578d3d15 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/ClientData.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import com.google.i18n.addressinput.common.LookupKey.KeyType; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +/** + * Access point for the cached address verification data. The data contained here will mainly be + * used to build {@link FieldVerifier}'s. + */ +public final class ClientData implements DataSource { + private static final Logger logger = Logger.getLogger(ClientData.class.getName()); + + /** + * Data to bootstrap the process. The data are all regional (country level) data. Keys are like + * "data/US/CA" + */ + private final Map<String, JsoMap> bootstrapMap = new HashMap<String, JsoMap>(); + + private CacheData cacheData; + + public ClientData(CacheData cacheData) { + this.cacheData = cacheData; + buildRegionalData(); + } + + @Override + public AddressVerificationNodeData get(String key) { + JsoMap jso = cacheData.getObj(key); + if (jso == null) { // Not cached. + fetchDataIfNotAvailable(key); + jso = cacheData.getObj(key); + } + if (jso != null && isValidDataKey(key)) { + return createNodeData(jso); + } + return null; + } + + @Override + public AddressVerificationNodeData getDefaultData(String key) { + // root data + if (key.split("/").length == 1) { + JsoMap jso = bootstrapMap.get(key); + if (jso == null || !isValidDataKey(key)) { + throw new RuntimeException("key " + key + " does not have bootstrap data"); + } + return createNodeData(jso); + } + + key = getCountryKey(key); + JsoMap jso = bootstrapMap.get(key); + if (jso == null || !isValidDataKey(key)) { + throw new RuntimeException("key " + key + " does not have bootstrap data"); + } + return createNodeData(jso); + } + + private String getCountryKey(String hierarchyKey) { + if (hierarchyKey.split("/").length <= 1) { + throw new RuntimeException("Cannot get country key with key '" + hierarchyKey + "'"); + } + if (isCountryKey(hierarchyKey)) { + return hierarchyKey; + } + + String[] parts = hierarchyKey.split("/"); + return parts[0] + "/" + parts[1]; + } + + private boolean isCountryKey(String hierarchyKey) { + Util.checkNotNull(hierarchyKey, "Cannot use null as a key"); + return hierarchyKey.split("/").length == 2; + } + + /** + * Returns the contents of the JSON-format string as a map. + */ + protected AddressVerificationNodeData createNodeData(JsoMap jso) { + Map<AddressDataKey, String> map = new EnumMap<AddressDataKey, String>(AddressDataKey.class); + + JSONArray arr = jso.getKeys(); + for (int i = 0; i < arr.length(); i++) { + try { + AddressDataKey key = AddressDataKey.get(arr.getString(i)); + + if (key == null) { + // Not all keys are supported by Android, so we continue if we encounter one + // that is not used. + continue; + } + + String value = jso.get(Util.toLowerCaseLocaleIndependent(key.toString())); + map.put(key, value); + } catch (JSONException e) { + // This should not happen - we should not be fetching a key from outside the bounds + // of the array. + } + } + + return new AddressVerificationNodeData(map); + } + + /** + * We can be initialized with the full set of address information, but validation only uses info + * prefixed with "data" (in particular, no info prefixed with "examples"). + */ + private boolean isValidDataKey(String key) { + return key.startsWith("data"); + } + + /** + * Initializes regionalData structure based on property file. + */ + private void buildRegionalData() { + StringBuilder countries = new StringBuilder(); + + for (String countryCode : RegionDataConstants.getCountryFormatMap().keySet()) { + countries.append(countryCode + "~"); + String json = RegionDataConstants.getCountryFormatMap().get(countryCode); + JsoMap jso = null; + try { + jso = JsoMap.buildJsoMap(json); + } catch (JSONException e) { + // Ignore. + } + + AddressData data = new AddressData.Builder().setCountry(countryCode).build(); + LookupKey key = new LookupKey.Builder(KeyType.DATA).setAddressData(data).build(); + bootstrapMap.put(key.toString(), jso); + } + countries.setLength(countries.length() - 1); + + // TODO: this is messy. do we have better ways to do it? + // Creates verification data for key="data". This will be used for the root FieldVerifier. + String str = "{\"id\":\"data\",\"countries\": \"" + countries.toString() + "\"}"; + JsoMap jsoData = null; + try { + jsoData = JsoMap.buildJsoMap(str); + } catch (JSONException e) { + // Ignore. + } + bootstrapMap.put("data", jsoData); + } + + /** + * Fetches data from remote server if it is not cached yet. + * + * @param key The key for data that being requested. Key can be either a data key (starts with + * "data") or example key (starts with "examples") + */ + private void fetchDataIfNotAvailable(String key) { + JsoMap jso = cacheData.getObj(key); + if (jso == null) { + // If there is bootstrap data for the key, pass the data to fetchDynamicData + JsoMap regionalData = bootstrapMap.get(key); + NotifyingListener listener = new NotifyingListener(); + // If the key was invalid, we don't want to attempt to fetch it. + if (LookupKey.hasValidKeyPrefix(key)) { + LookupKey lookupKey = new LookupKey.Builder(key).build(); + cacheData.fetchDynamicData(lookupKey, regionalData, listener); + try { + listener.waitLoadingEnd(); + // Check to see if there is data for this key now. + if (cacheData.getObj(key) == null && isCountryKey(key)) { + // If not, see if there is data in RegionDataConstants. + logger.info("Server failure: looking up key in region data constants."); + cacheData.getFromRegionDataConstants(lookupKey); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + public void requestData(LookupKey key, DataLoadListener listener) { + Util.checkNotNull(key, "Null lookup key not allowed"); + JsoMap regionalData = bootstrapMap.get(key.toString()); + cacheData.fetchDynamicData(key, regionalData, listener); + } + + /** + * Fetches all data for the specified country from the remote server. + */ + public void prefetchCountry(String country, DataLoadListener listener) { + String key = "data/" + country; + Set<RecursiveLoader> loaders = new HashSet<RecursiveLoader>(); + listener.dataLoadingBegin(); + cacheData.fetchDynamicData( + new LookupKey.Builder(key).build(), null, new RecursiveLoader(key, loaders, listener)); + } + + /** + * A helper class to recursively load all sub keys using fetchDynamicData(). + */ + private class RecursiveLoader implements DataLoadListener { + private final String key; + + private final Set<RecursiveLoader> loaders; + + private final DataLoadListener listener; + + public RecursiveLoader(String key, Set<RecursiveLoader> loaders, DataLoadListener listener) { + this.key = key; + this.loaders = loaders; + this.listener = listener; + + synchronized (loaders) { + loaders.add(this); + } + } + + @Override + public void dataLoadingBegin() { + } + + @Override + public void dataLoadingEnd() { + final String subKeys = Util.toLowerCaseLocaleIndependent(AddressDataKey.SUB_KEYS.name()); + final String subMores = Util.toLowerCaseLocaleIndependent(AddressDataKey.SUB_MORES.name()); + + JsoMap map = cacheData.getObj(key); + // It is entirely possible that data loading failed and that the map is null. + if (map != null && map.containsKey(subMores)) { + // This key could have sub keys. + String[] mores = map.get(subMores).split("~"); + String[] keys = {}; + + if (map.containsKey(subKeys)) { + keys = map.get(subKeys).split("~"); + } + + if (mores.length != keys.length) { // This should never happen. + throw new IndexOutOfBoundsException("mores.length != keys.length"); + } + + for (int i = 0; i < mores.length; i++) { + if (mores[i].equalsIgnoreCase("true")) { + // This key should have sub keys. + String subKey = key + "/" + keys[i]; + cacheData.fetchDynamicData(new LookupKey.Builder(subKey).build(), null, + new RecursiveLoader(subKey, loaders, listener)); + } + } + } + + // TODO: Rethink how notification is handled to avoid error-prone synchronization. + boolean wasLastLoader = false; + synchronized (loaders) { + wasLastLoader = loaders.remove(this) && loaders.isEmpty(); + } + if (wasLastLoader) { + listener.dataLoadingEnd(); + } + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataLoadListener.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataLoadListener.java new file mode 100644 index 00000000000..89997b92fa0 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataLoadListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * Invoked when the data is fetched from the server or the cache. + */ +public interface DataLoadListener { + // These callbacks are invoked from a background thread. + void dataLoadingBegin(); + void dataLoadingEnd(); +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataSource.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataSource.java new file mode 100644 index 00000000000..21d677caaa7 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/DataSource.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * API for returning JSON content for a given key string (eg, "data/US"). The content is + * returned as a map within an {@link AddressVerificationNodeData} instance. This interface + * exists only to isolate this API and allow us to swap in a different version in future + * without risking callers depending on unexpected parts of the verifier API. + */ +// TODO: Add a way to load static data without using the AddressVerificationData and remove +// this interface (along with AddressVerificationData). +public interface DataSource { + /** + * Returns the default JSON data for the given key string (this method should complete immediately + * and must not trigger any network requests. + */ + AddressVerificationNodeData getDefaultData(String key); + + /** + * A <b>blocking</b> method to return the JSON data for the given key string. This method will + * block the current thread until data is available or until a timeout occurs (at which point the + * default data will be returned). All networking and failure states are hidden from the caller by + * this API. + */ + // TODO: This is very poor API and should be changed to avoid blocking and let the caller + // manage requests asynchronously. + AddressVerificationNodeData get(String key); +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java new file mode 100644 index 00000000000..7ee0151ca64 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import com.google.i18n.addressinput.common.LookupKey.KeyType; +import com.google.i18n.addressinput.common.LookupKey.ScriptType; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Accesses address verification data used to verify components of an address. + * <p> + * Not all fields require all types of validation, although this could be done. In particular, + * the current implementation only provides known value verification for the hierarchical fields, + * and only provides format and match verification for the postal code field. + */ +public final class FieldVerifier { + // A value for a particular language is has the language separated by this String. + private static final String LOCALE_DELIMITER = "--"; + // Node data values are delimited by this symbol. + private static final String LIST_DELIMITER = "~"; + // Keys are built up using this delimiter: eg data/US, data/US/CA. + private static final String KEY_NODE_DELIMITER = "/"; + + private static final FormatInterpreter FORMAT_INTERPRETER = + new FormatInterpreter(new FormOptions().createSnapshot()); + + // Package-private so it can be accessed by tests. + String id; + private DataSource dataSource; + private boolean useRegionDataConstants; + + // Package-private so they can be accessed by tests. + Set<AddressField> possiblyUsedFields; + Set<AddressField> required; + // Known values. Can be either a key, a name in Latin, or a name in native script. + private Map<String, String> candidateValues; + + // Keys for the subnodes of this verifier. For example, a key for the US would be CA, since + // there is a sub-verifier with the ID "data/US/CA". Keys may be the local names of the + // locations in the next level of the hierarchy, or the abbreviations if suitable abbreviations + // exist. Package-private so it can be accessed by tests. + String[] keys; + // Names in Latin. These are only populated if the native/local names are in a script other than + // latin. + private String[] latinNames; + // Names in native script. + private String[] localNames; + + // Pattern representing the format of a postal code number. + private Pattern format; + // Defines the valid range of a postal code number. + private Pattern match; + + /** + * Creates the root field verifier for a particular data source. Defaults useRegionDataConstants + * to true. + */ + public FieldVerifier(DataSource dataSource) { + this(dataSource, true /* useRegionDataConstants */); + } + + /** + * Creates the root field verifier for a particular data source. + */ + public FieldVerifier(DataSource dataSource, boolean useRegionDataConstants) { + this.dataSource = dataSource; + this.useRegionDataConstants = useRegionDataConstants; + populateRootVerifier(); + } + + /** + * Creates a field verifier based on its parent and on the new data for this node supplied by + * nodeData (which may be null). + * Package-private so it can be accessed by tests. + */ + FieldVerifier(FieldVerifier parent, AddressVerificationNodeData nodeData) { + // Most information is inherited from the parent. + possiblyUsedFields = new HashSet<AddressField>(parent.possiblyUsedFields); + required = new HashSet<AddressField>(parent.required); + dataSource = parent.dataSource; + useRegionDataConstants = parent.useRegionDataConstants; + format = parent.format; + match = parent.match; + // Here we add in any overrides from this particular node as well as information such as + // localNames, latinNames and keys. + populate(nodeData); + // candidateValues should never be inherited from the parent, but built up from the + // localNames in this node. + candidateValues = Util.buildNameToKeyMap(keys, localNames, latinNames); + } + + /** + * Sets possiblyUsedFields, required, keys and candidateValues for the root field verifier. + */ + private void populateRootVerifier() { + id = "data"; + // Keys come from the countries under "data". + AddressVerificationNodeData rootNode = dataSource.getDefaultData("data"); + if (rootNode.containsKey(AddressDataKey.COUNTRIES)) { + keys = rootNode.get(AddressDataKey.COUNTRIES).split(LIST_DELIMITER); + } + // candidateValues is just the set of keys. + candidateValues = Util.buildNameToKeyMap(keys, null, null); + + // TODO: Investigate if these need to be set here. The country level population already + // handles the fallback, the question is if validation can be done without a country level + // validator being created. + // Copy "possiblyUsedFields" and "required" from the defaults here for bootstrapping. + possiblyUsedFields = new HashSet<AddressField>(); + required = new HashSet<AddressField>(); + populatePossibleAndRequired("ZZ"); + } + + /** + * Populates this verifier with data from the node data passed in and from RegionDataConstants. + * The node data may be null. + */ + private void populate(AddressVerificationNodeData nodeData) { + if (nodeData == null) { + return; + } + if (nodeData.containsKey(AddressDataKey.ID)) { + id = nodeData.get(AddressDataKey.ID); + } + if (nodeData.containsKey(AddressDataKey.SUB_KEYS)) { + keys = nodeData.get(AddressDataKey.SUB_KEYS).split(LIST_DELIMITER); + } + if (nodeData.containsKey(AddressDataKey.SUB_LNAMES)) { + latinNames = nodeData.get(AddressDataKey.SUB_LNAMES).split(LIST_DELIMITER); + } + if (nodeData.containsKey(AddressDataKey.SUB_NAMES)) { + localNames = nodeData.get(AddressDataKey.SUB_NAMES).split(LIST_DELIMITER); + } + if (nodeData.containsKey(AddressDataKey.XZIP)) { + format = Pattern.compile(nodeData.get(AddressDataKey.XZIP), Pattern.CASE_INSENSITIVE); + } + if (nodeData.containsKey(AddressDataKey.ZIP)) { + // This key has two different meanings, depending on whether this is a country-level key + // or not. + if (isCountryKey()) { + format = Pattern.compile(nodeData.get(AddressDataKey.ZIP), Pattern.CASE_INSENSITIVE); + } else { + match = Pattern.compile(nodeData.get(AddressDataKey.ZIP), Pattern.CASE_INSENSITIVE); + } + } + // If there are latin names but no local names, and there are the same number of latin names + // as there are keys, then we assume the local names are the same as the keys. + if (keys != null && localNames == null && latinNames != null + && keys.length == latinNames.length) { + localNames = keys; + } + if (isCountryKey()) { + populatePossibleAndRequired(getRegionCodeFromKey(id)); + } + } + + /** + * This method assumes the hierarchyKey contains a region code. If not, returns ZZ. + */ + private static String getRegionCodeFromKey(String hierarchyKey) { + String[] parts = hierarchyKey.split(KEY_NODE_DELIMITER); + if (parts.length == 1) { + // Return the unknown region if none was found. + return "ZZ"; + } + return parts[1].split(LOCALE_DELIMITER)[0]; + } + + // TODO: We should be consistent with where the language data comes from; what are the + // consequences if the server is out-of-sync with the client? We should get the language from the + // same place here and in FormController; it's not obvious that happens right now. + private Set<String> getAcceptableAlternateLanguages(String regionCode) { + // TODO: We should have a class that knows how to get information about the data, rather than + // getting the node and extracting keys here. + AddressVerificationNodeData countryNode = getCountryNode(regionCode); + String languages = countryNode.get(AddressDataKey.LANGUAGES); + String defaultLanguage = countryNode.get(AddressDataKey.LANG); + Set<String> alternateLanguages = new HashSet<String>(); + // If languages is set, defaultLanguage will be set as well. + if (languages != null && defaultLanguage != null) { + String languagesArray[] = languages.split(LIST_DELIMITER); + for (String lang : languagesArray) { + // The default language is never appended to keys. + if (!lang.equals(defaultLanguage)) { + alternateLanguages.add(lang); + } + } + } + return alternateLanguages; + } + + private AddressVerificationNodeData getCountryNode(String regionCode) { + LookupKey lookupKey = new LookupKey.Builder(KeyType.DATA) + .setAddressData(new AddressData.Builder().setCountry(regionCode).build()) + .build(); + return dataSource.getDefaultData(lookupKey.toString()); + } + + private void populatePossibleAndRequired(String regionCode) { + // If useRegionDataConstants is true, these fields are populated from RegionDataConstants so + // that the metadata server can be updated without needing to be in sync with clients; + // otherwise, these fields are populated from dataSource. + if (!useRegionDataConstants) { + AddressVerificationNodeData countryNode = getCountryNode(regionCode); + AddressVerificationNodeData defaultNode = getCountryNode("ZZ"); + + String formatString = countryNode.get(AddressDataKey.FMT); + if (formatString == null) { + formatString = defaultNode.get(AddressDataKey.FMT); + } + if (formatString != null) { + List<AddressField> possible = + FORMAT_INTERPRETER.getAddressFieldOrder(formatString, regionCode); + possiblyUsedFields.addAll(convertAddressFieldsToPossiblyUsedSet(possible)); + } /* else: shouldn't ever happen */ + String requireString = countryNode.get(AddressDataKey.REQUIRE); + if (requireString == null) { + requireString = defaultNode.get(AddressDataKey.REQUIRE); + } + if (requireString != null) { + required = FormatInterpreter.getRequiredFields(requireString, regionCode); + } /* else: shouldn't ever happen */ + return; + } + + List<AddressField> possible = + FORMAT_INTERPRETER.getAddressFieldOrder(ScriptType.LOCAL, regionCode); + possiblyUsedFields = convertAddressFieldsToPossiblyUsedSet(possible); + required = FormatInterpreter.getRequiredFields(regionCode); + } + + FieldVerifier refineVerifier(String sublevel) { + if (Util.trimToNull(sublevel) == null || id == null) { + return new FieldVerifier(this, null); + } + + // Split the subkey into key + language (if any). Check the language is an acceptable + // alternative for the region, for which we have data. If not, we drop it from the data key. + String[] parts = sublevel.split(LOCALE_DELIMITER); + + if (parts.length == 0){ + // May only contains the LOCALE_DELIMITER. + return new FieldVerifier(this, null); + } + + // Makes the new key - the old key, plus the new data, minus the language code. + String currentFullKey = id + KEY_NODE_DELIMITER + parts[0]; + + // If a language was present, check that it is valid. + if (parts.length > 1) { + // Since currentFullKey must have the KEY_NODE_DELIMITER - we added it above - this is safe. + String regionCode = getRegionCodeFromKey(currentFullKey); + if (getAcceptableAlternateLanguages(regionCode).contains(parts[1])) { + currentFullKey = currentFullKey + LOCALE_DELIMITER + parts[1]; + } + } + + // This fixes the position of the language in the key, so data/CA--fr/Quebec would be + // canonicalised to data/CA/Quebec--fr. + currentFullKey = new LookupKey.Builder(currentFullKey).build().toString(); + // For names with no Latin equivalent, we can look up the sublevel name directly. + AddressVerificationNodeData nodeData = dataSource.get(currentFullKey); + if (nodeData != null) { + return new FieldVerifier(this, nodeData); + } + // If that failed, then we try to look up the local name equivalent of this latin name. + // First check these exist. + if (latinNames == null) { + return new FieldVerifier(this, null); + } + for (int n = 0; n < latinNames.length; n++) { + if (latinNames[n].equalsIgnoreCase(sublevel)) { + // We found a match - we should try looking up a key with the local name at the same + // index. + currentFullKey = + new LookupKey.Builder(id + KEY_NODE_DELIMITER + localNames[n]).build().toString(); + nodeData = dataSource.get(currentFullKey); + if (nodeData != null) { + return new FieldVerifier(this, nodeData); + } + } + } + // No sub-verifiers were found. + return new FieldVerifier(this, null); + } + + /** + * Returns the ID of this verifier. + */ + @Override + public String toString() { + return id; + } + + /** + * Checks a value in a particular script for a particular field to see if it causes the problem + * specified. If so, this problem is added to the AddressProblems collection passed in. Returns + * true if no problem was found. + * + * @param script the script type used to verify address. This affects countries + * where there are values in the local language and in latin script, such as China. + * If null, do not consider script type, so both latin and local language values would be + * considered valid. + * @param problem problem type to check. For example, when problem type is + * {@code UNEXPECTED_FIELD}, checks that the input {@code field} is not used. + * @param field address field to verify. + * @param value field value. + * @param problems collection of problems collected during verification. + * @return true if verification passes. + */ + protected boolean check(ScriptType script, AddressProblemType problem, AddressField field, + String value, AddressProblems problems) { + boolean problemFound = false; + + String trimmedValue = Util.trimToNull(value); + switch (problem) { + case UNEXPECTED_FIELD: + if (trimmedValue != null && !possiblyUsedFields.contains(field)) { + problemFound = true; + } + break; + case MISSING_REQUIRED_FIELD: + if (required.contains(field) && trimmedValue == null) { + problemFound = true; + } + break; + case UNKNOWN_VALUE: + // An empty string will never be an UNKNOWN_VALUE. It is invalid + // only when it appears in a required field (In that case it will + // be reported as MISSING_REQUIRED_FIELD). + if (trimmedValue == null) { + break; + } + problemFound = !isKnownInScript(script, trimmedValue); + break; + case INVALID_FORMAT: + if (trimmedValue != null && format != null && !format.matcher(trimmedValue).matches()) { + problemFound = true; + } + break; + case MISMATCHING_VALUE: + if (trimmedValue != null && match != null && !match.matcher(trimmedValue).lookingAt()) { + problemFound = true; + } + break; + default: + throw new RuntimeException("Unknown problem: " + problem); + } + if (problemFound) { + problems.add(field, problem); + } + return !problemFound; + } + + /** + * Checks the value of a particular field in a particular script against the known values for + * this field. If script is null, it checks both the local and the latin values. Otherwise it + * checks only the values in the script specified. + */ + private boolean isKnownInScript(ScriptType script, String value) { + String trimmedValue = Util.trimToNull(value); + Util.checkNotNull(trimmedValue); + if (script == null) { + return (candidateValues == null || candidateValues.containsKey( + Util.toLowerCaseLocaleIndependent(trimmedValue))); + } + // Otherwise, if we know the script, we want to restrict the candidates to only names in + // that script. + String[] namesToConsider = (script == ScriptType.LATIN) ? latinNames : localNames; + Set<String> candidates = new HashSet<String>(); + if (namesToConsider != null) { + for (String name : namesToConsider) { + candidates.add(Util.toLowerCaseLocaleIndependent(name)); + } + } + if (keys != null) { + for (String name : keys) { + candidates.add(Util.toLowerCaseLocaleIndependent(name)); + } + } + + if (candidates.size() == 0 || trimmedValue == null) { + return true; + } + + return candidates.contains(Util.toLowerCaseLocaleIndependent(value)); + } + + /** + * Converts a list of address fields to a set of possibly used fields. Adds country and handles + * street address. + */ + private static Set<AddressField> convertAddressFieldsToPossiblyUsedSet( + List<AddressField> fields) { + // COUNTRY is never unexpected. + EnumSet<AddressField> result = EnumSet.of(AddressField.COUNTRY); + for (AddressField field : fields) { + // Replace ADDRESS_LINE with STREET_ADDRESS because that's what the validation expects. + if (field == AddressField.ADDRESS_LINE_1 || field == AddressField.ADDRESS_LINE_2) { + result.add(AddressField.STREET_ADDRESS); + } else { + result.add(field); + } + } + return result; + } + + /** + * Returns true if this key represents a country. We assume all keys with only one delimiter are + * at the country level (such as "data/US"). + */ + private boolean isCountryKey() { + Util.checkNotNull(id, "Cannot use null as key"); + return id.split(KEY_NODE_DELIMITER).length == 2; + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormController.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormController.java new file mode 100644 index 00000000000..1f80e21a6f5 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormController.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import com.google.i18n.addressinput.common.LookupKey.KeyType; +import com.google.i18n.addressinput.common.LookupKey.ScriptType; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * Responsible for looking up data for address fields. This fetches possible + * values for the next level down in the address hierarchy, if these are known. + */ +public final class FormController { + // For address hierarchy in lookup key. + private static final String SLASH_DELIM = "/"; + // For joined values. + private static final String TILDE_DELIM = "~"; + // For language code info in lookup key (E.g., data/CA--fr). + private static final String DASH_DELIM = "--"; + private static final LookupKey ROOT_KEY = FormController.getDataKeyForRoot(); + private static final String DEFAULT_REGION_CODE = "ZZ"; + private static final AddressField[] ADDRESS_HIERARCHY = { + AddressField.COUNTRY, + AddressField.ADMIN_AREA, + AddressField.LOCALITY, + AddressField.DEPENDENT_LOCALITY}; + + // Current user language. + private String languageCode; + private final ClientData integratedData; + private String currentCountry; + + /** + * Constructor that populates this with data. languageCode should be a BCP language code (such + * as "en" or "zh-Hant") and currentCountry should be an ISO 2-letter region code (such as "GB" + * or "US"). + */ + public FormController(ClientData integratedData, String languageCode, String currentCountry) { + Util.checkNotNull(integratedData, "null data not allowed"); + this.languageCode = languageCode; + this.currentCountry = currentCountry; + + AddressData address = new AddressData.Builder().setCountry(DEFAULT_REGION_CODE).build(); + LookupKey defaultCountryKey = getDataKeyFor(address); + + AddressVerificationNodeData defaultCountryData = + integratedData.getDefaultData(defaultCountryKey.toString()); + Util.checkNotNull(defaultCountryData, + "require data for default country key: " + defaultCountryKey); + this.integratedData = integratedData; + } + + public void setLanguageCode(String languageCode) { + this.languageCode = languageCode; + } + + public void setCurrentCountry(String currentCountry) { + this.currentCountry = currentCountry; + } + + private ScriptType getScriptType() { + if (languageCode != null && Util.isExplicitLatinScript(languageCode)) { + return ScriptType.LATIN; + } + return ScriptType.LOCAL; + } + + private static LookupKey getDataKeyForRoot() { + AddressData address = new AddressData.Builder().build(); + return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build(); + } + + public LookupKey getDataKeyFor(AddressData address) { + return new LookupKey.Builder(KeyType.DATA).setAddressData(address).build(); + } + + /** + * Requests data for the input address. This method chains multiple requests together. For + * example, an address for Mt View, California needs data from "data/US", "data/US/CA", and + * "data/US/CA/Mt View" to support it. This method will request them one by one (from top level + * key down to the most granular) and evokes {@link DataLoadListener#dataLoadingEnd} method when + * all data is collected. If the address is invalid, it will request the first valid child key + * instead. For example, a request for "data/US/Foo" will end up requesting data for "data/US", + * "data/US/AL". + * + * @param address the current address. + * @param listener triggered when requested data for the address is returned. + */ + public void requestDataForAddress(AddressData address, DataLoadListener listener) { + Util.checkNotNull(address.getPostalCountry(), "null country not allowed"); + + // Gets the key for deepest available node. + Queue<String> subkeys = new LinkedList<String>(); + + for (AddressField field : ADDRESS_HIERARCHY) { + String value = address.getFieldValue(field); + if (value == null) { + break; + } + subkeys.add(value); + } + if (subkeys.size() == 0) { + throw new RuntimeException("Need at least country level info"); + } + + if (listener != null) { + listener.dataLoadingBegin(); + } + requestDataRecursively(ROOT_KEY, subkeys, listener); + } + + private void requestDataRecursively( + final LookupKey key, final Queue<String> subkeys, final DataLoadListener listener) { + Util.checkNotNull(key, "Null key not allowed"); + Util.checkNotNull(subkeys, "Null subkeys not allowed"); + + integratedData.requestData(key, new DataLoadListener() { + @Override + public void dataLoadingBegin() { + } + + @Override + public void dataLoadingEnd() { + List<RegionData> subregions = getRegionData(key); + if (subregions.isEmpty()) { + if (listener != null) { + listener.dataLoadingEnd(); + } + // TODO: Should update the selectors here. + return; + } else if (subkeys.size() > 0) { + String subkey = subkeys.remove(); + for (RegionData subregion : subregions) { + if (subregion.isValidName(subkey)) { + LookupKey nextKey = buildDataLookupKey(key, subregion.getKey()); + requestDataRecursively(nextKey, subkeys, listener); + return; + } + } + } + + // Current value in the field is not valid, use the first valid subkey + // to request more data instead. + String firstSubkey = subregions.get(0).getKey(); + LookupKey nextKey = buildDataLookupKey(key, firstSubkey); + Queue<String> emptyList = new LinkedList<String>(); + requestDataRecursively(nextKey, emptyList, listener); + } + }); + } + + private LookupKey buildDataLookupKey(LookupKey lookupKey, String subKey) { + String[] subKeys = lookupKey.toString().split(SLASH_DELIM); + String languageCodeSubTag = + (languageCode == null) ? null : Util.getLanguageSubtag(languageCode); + String key = lookupKey.toString() + SLASH_DELIM + subKey; + + // Country level key + if (subKeys.length == 1 && languageCodeSubTag != null + && !isDefaultLanguage(languageCodeSubTag)) { + key += DASH_DELIM + languageCodeSubTag.toString(); + } + return new LookupKey.Builder(key).build(); + } + + /** + * Compares the language subtags of input {@code languageCode} and default language code. For + * example, "zh-Hant" and "zh" are viewed as identical. + */ + public boolean isDefaultLanguage(String languageCode) { + if (languageCode == null) { + return true; + } + AddressData addr = new AddressData.Builder().setCountry(currentCountry).build(); + LookupKey lookupKey = getDataKeyFor(addr); + AddressVerificationNodeData data = integratedData.getDefaultData(lookupKey.toString()); + String defaultLanguage = data.get(AddressDataKey.LANG); + + // Current language is not the default language for the country. + if (Util.trimToNull(defaultLanguage) != null + && !Util.getLanguageSubtag(languageCode).equals(Util.getLanguageSubtag(languageCode))) { + return false; + } + return true; + } + + /** + * Gets a list of {@link RegionData} for sub-regions for a given key. For example, sub regions + * for "data/US" are AL/Alabama, AK/Alaska, etc. + * + * <p> TODO: Rename/simplify RegionData to avoid confusion with RegionDataConstants elsewhere + * since it does not contain anything more than key/value pairs now. + * + * @return A list of sub-regions, each sub-region represented by a {@link RegionData}. + */ + public List<RegionData> getRegionData(LookupKey key) { + if (key.getKeyType() == KeyType.EXAMPLES) { + throw new RuntimeException("example key not allowed for getting region data"); + } + Util.checkNotNull(key, "null regionKey not allowed"); + LookupKey normalizedKey = normalizeLookupKey(key); + List<RegionData> results = new ArrayList<RegionData>(); + + // Root key. + if (normalizedKey.equals(ROOT_KEY)) { + AddressVerificationNodeData data = integratedData.getDefaultData(normalizedKey.toString()); + String[] countries = splitData(data.get(AddressDataKey.COUNTRIES)); + for (int i = 0; i < countries.length; i++) { + RegionData rd = new RegionData.Builder().setKey(countries[i]).setName(countries[i]).build(); + results.add(rd); + } + return results; + } + + AddressVerificationNodeData data = integratedData.get(normalizedKey.toString()); + if (data != null) { + String[] keys = splitData(data.get(AddressDataKey.SUB_KEYS)); + String[] names = (getScriptType() == ScriptType.LOCAL) + ? splitData(data.get(AddressDataKey.SUB_NAMES)) + : splitData(data.get(AddressDataKey.SUB_LNAMES)); + + for (int i = 0; i < keys.length; i++) { + RegionData rd = new RegionData.Builder() + .setKey(keys[i]) + .setName((i < names.length) ? names[i] : keys[i]) + .build(); + results.add(rd); + } + } + return results; + } + + /** + * Split a '~' delimited string into an array of strings. This method is null tolerant and + * considers an empty string to contain no elements. + * + * @param raw The data to split + * @return an array of strings + */ + private String[] splitData(String raw) { + if (raw == null || raw.isEmpty()) { + return new String[]{}; + } + return raw.split(TILDE_DELIM); + } + + private String getSubKey(LookupKey parentKey, String name) { + for (RegionData subRegion : getRegionData(parentKey)) { + if (subRegion.isValidName(name)) { + return subRegion.getKey(); + } + } + return null; + } + + /** + * Normalizes {@code key} by replacing field values with sub-keys. For example, California is + * replaced with CA. The normalization goes from top (country) to bottom (dependent locality) + * and if any field value is empty, unknown, or invalid, it will stop and return whatever it + * gets. For example, a key "data/US/California/foobar/kar" will be normalized into + * "data/US/CA/foobar/kar" since "foobar" is unknown. This method supports only keys of + * {@link KeyType#DATA} type. + * + * @return normalized {@link LookupKey}. + */ + private LookupKey normalizeLookupKey(LookupKey key) { + Util.checkNotNull(key); + if (key.getKeyType() != KeyType.DATA) { + throw new RuntimeException("Only DATA keyType is supported"); + } + + String subStr[] = key.toString().split(SLASH_DELIM); + + // Root key does not need to be normalized. + if (subStr.length < 2) { + return key; + } + + StringBuilder sb = new StringBuilder(subStr[0]); + for (int i = 1; i < subStr.length; ++i) { + // Strips the language code if there was one. + String languageCode = null; + if (i == 1 && subStr[i].contains(DASH_DELIM)) { + String[] s = subStr[i].split(DASH_DELIM); + subStr[i] = s[0]; + languageCode = s[1]; + } + + String normalizedSubKey = getSubKey(new LookupKey.Builder(sb.toString()).build(), subStr[i]); + + // Can't find normalized sub-key; assembles the lookup key with the + // remaining sub-keys and returns it. + if (normalizedSubKey == null) { + for (; i < subStr.length; ++i) { + sb.append(SLASH_DELIM).append(subStr[i]); + } + break; + } + sb.append(SLASH_DELIM).append(normalizedSubKey); + if (languageCode != null) { + sb.append(DASH_DELIM).append(languageCode); + } + } + return new LookupKey.Builder(sb.toString()).build(); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormOptions.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormOptions.java new file mode 100644 index 00000000000..0704fc6a790 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormOptions.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Configuration options for the address input widget used to control the visibility and interaction + * of specific fields to suit specific use cases (eg, collecting business addresses, collecting + * addresses for credit card verification etc...). + * <p> + * When form options are passed to the address widget a snapshot is taken and any further changes to + * the options are ignored. + * <p> + * This design is somewhat like using a builder but has the advantage that the caller only sees the + * outer (mutable) type and never needs to know about the "built" snapshot. This reduces the public + * API footprint and simplifies usage of this class. + */ +// This is an external class and part of the widget's public API. +public final class FormOptions { + + // These fields must never be null). + private Set<AddressField> hiddenFields = EnumSet.noneOf(AddressField.class); + private Set<AddressField> readonlyFields = EnumSet.noneOf(AddressField.class); + private Set<String> blacklistedRegions = new HashSet<String>(); + // Key is ISO 2-letter region code. + private Map<String, List<AddressField>> customFieldOrder = + new HashMap<String, List<AddressField>>(); + + /** Creates an empty, mutable, form options instance. */ + public FormOptions() { + } + + /** + * Hides the given address field. Calls to this method <strong>are cumulative</strong>. Fields + * which are specified here but not part of a country's specified fields will be ignored. + * <p> + * Note also that hiding fields occurs after custom ordering has been applied, although combining + * these two features is not generally recommended due to the confusion it is likely to cause. + */ + public FormOptions setHidden(AddressField field) { + hiddenFields.add(field); + return this; + } + + /** + * Sets the given address field as read-only. Calls to this method <strong>are cumulative + * </strong>. Fields which are specified here but not part of a country's specified fields will be + * ignored. + * <p> + * This method is j2objc- & iOS API friendly as the signature does not expose varargs / Java + * arrays or collections. + */ + public FormOptions setReadonly(AddressField field) { + readonlyFields.add(field); + return this; + } + + /** + * Sets the order of address input fields for the given ISO 3166-1 two letter country code. + * <p> + * Input fields affected by custom ordering will be shown in the widget in the order they are + * given to this method (for the associated region code). Fields which are visible for a region, + * but which are not specified here, will appear in their original position in the form. For + * example, if a region defines the following fields: + * <pre> + * [ RECIPIENT -> ORGANIZATION -> STREET_ADDRESS -> LOCALITY -> ADMIN_AREA -> COUNTRY ] + * </pre> + * and the custom ordering for that region is (somewhat contrived): + * <pre> + * [ ORGANIZATION -> COUNTRY -> RECIPIENT ] + * </pre> + * Then the visible order of the input fields will be: + * <pre> + * [ ORGANIZATION -> COUNTRY -> STREET_ADDRESS -> LOCALITY -> ADMIN_AREA -> RECIPIENT ] + * </pre> + * <ul> + * <li>Fields not specified in the custom ordering (STREET_ADDRESS, LOCALITY, ADMIN_AREA) + * remain in their original, absolute, positions. + * <li>Custom ordered fields are re-positioned such that their relative order is now as + * specified (but other, non custom-ordered, fields can appear between them). + * </ul> + * <p> + * If the custom order contains a field which is not present for the specified region, it is + * silently ignored. Setting a custom ordering can never be used as a way to add fields for a + * region. + * <p> + * Typically this feature is used to reverse things like RECIPIENT and ORGANIZATION for certain + * business related use cases. It should not be used to "correct" perceived bad field ordering + * or make different countries "more consistent with each other". + */ + public FormOptions setCustomFieldOrder(String regionCode, AddressField... fields) { + // TODO: Consider checking the given region code for validity against RegionDataConstants. + List<AddressField> fieldList = Collections.unmodifiableList(Arrays.asList(fields)); + if (fieldList.size() > 0) { + if (EnumSet.copyOf(fieldList).size() != fieldList.size()) { + throw new IllegalArgumentException("duplicate address field: " + fieldList); + } + customFieldOrder.put(regionCode, fieldList); + } else { + customFieldOrder.remove(regionCode); + } + return this; + } + + /** + * Blacklist the given CLDR (Common Locale Data Repository) region (country) code + * indicating countries that for legal or other reasons should not be available. + * <p> + * Calls are cumulative, call this method once for each region that needs to be blacklisted. + * <p> + * We reserve the right to change this API from taking individual regions to taking a set. + */ + public FormOptions blacklistRegion(String regionCode) { + if (regionCode == null) { + throw new NullPointerException(); + } + // TODO(addresswidget-team): Add region code validation against RegionDataConstants. + blacklistedRegions.add(Util.toUpperCaseLocaleIndependent(regionCode)); + return this; + } + + /** Returns an immutable snapshot of the current state of the form options. */ + public Snapshot createSnapshot() { + return new Snapshot(this); + } + + /** An immutable snapshot of a {@code FormOptions} instance. */ + public static class Snapshot { + private final Set<AddressField> hiddenFields; + private final Set<AddressField> readonlyFields; + private final Set<String> blacklistedRegions; + private final Map<String, List<AddressField>> customFieldOrder; + + Snapshot(FormOptions options) { + this.hiddenFields = Collections.unmodifiableSet(EnumSet.copyOf(options.hiddenFields)); + this.readonlyFields = Collections.unmodifiableSet(EnumSet.copyOf(options.readonlyFields)); + // Shallow copy as lists are already immutable. + this.customFieldOrder = Collections.unmodifiableMap( + new HashMap<String, List<AddressField>>(options.customFieldOrder)); + this.blacklistedRegions = + Collections.unmodifiableSet(new HashSet<String>(options.blacklistedRegions)); + } + + public boolean isHidden(AddressField field) { + return hiddenFields.contains(field); + } + + public boolean isReadonly(AddressField field) { + return readonlyFields.contains(field); + } + + /** + * Gets the overridden field orders with their corresponding region code. Returns null if field + * orders for {@code regionCode} is not specified. + */ + List<AddressField> getCustomFieldOrder(String regionCode) { + return customFieldOrder.get(regionCode); + } + + public boolean isBlacklistedRegion(String regionCode) { + return blacklistedRegions.contains(Util.toUpperCaseLocaleIndependent(regionCode)); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java new file mode 100644 index 00000000000..ba4c668ffb4 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import com.google.i18n.addressinput.common.AddressField.WidthType; +import com.google.i18n.addressinput.common.LookupKey.ScriptType; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Address format interpreter. A utility to find address format related info. + */ +public final class FormatInterpreter { + private static final String NEW_LINE = "%n"; + + private final FormOptions.Snapshot formOptions; + + /** + * Creates a new instance of {@link FormatInterpreter}. + */ + public FormatInterpreter(FormOptions.Snapshot options) { + Util.checkNotNull( + RegionDataConstants.getCountryFormatMap(), "null country name map not allowed"); + Util.checkNotNull(options); + this.formOptions = options; + Util.checkNotNull(getJsonValue("ZZ", AddressDataKey.FMT), + "Could not obtain a default address field order."); + } + + /** + * Returns a list of address fields based on the format of {@code regionCode}. Script type is + * needed because some countries uses different address formats for local/Latin scripts. + * + * @param scriptType if {@link ScriptType#LOCAL}, use local format; else use Latin format. + */ + // TODO: Consider not re-doing this work every time the widget is re-rendered. + @SuppressWarnings("deprecation") // For legacy address fields. + public List<AddressField> getAddressFieldOrder(ScriptType scriptType, String regionCode) { + Util.checkNotNull(scriptType); + Util.checkNotNull(regionCode); + String formatString = getFormatString(scriptType, regionCode); + return getAddressFieldOrder(formatString, regionCode); + } + + List<AddressField> getAddressFieldOrder(String formatString, String regionCode) { + EnumSet<AddressField> visibleFields = EnumSet.noneOf(AddressField.class); + List<AddressField> fieldOrder = new ArrayList<AddressField>(); + // TODO: Change this to just enumerate the address fields directly. + for (String substring : getFormatSubstrings(formatString)) { + // Skips un-escaped characters and new lines. + if (!substring.matches("%.") || substring.equals(NEW_LINE)) { + continue; + } + AddressField field = getFieldForFormatSubstring(substring); + // Accept only the first instance for any duplicate fields (which can occur because the + // string we start with defines format order, which can contain duplicate fields). + if (!visibleFields.contains(field)) { + visibleFields.add(field); + fieldOrder.add(field); + } + } + applyFieldOrderOverrides(regionCode, fieldOrder); + + // Uses two address lines instead of street address. + for (int n = 0; n < fieldOrder.size(); n++) { + if (fieldOrder.get(n) == AddressField.STREET_ADDRESS) { + fieldOrder.set(n, AddressField.ADDRESS_LINE_1); + fieldOrder.add(n + 1, AddressField.ADDRESS_LINE_2); + break; + } + } + return Collections.unmodifiableList(fieldOrder); + } + + /** + * Returns true if this format substring (e.g. %C) represents an address field. Returns false if + * it is a literal or newline. + */ + private static boolean formatSubstringRepresentsField(String formatSubstring) { + return !formatSubstring.equals(NEW_LINE) && formatSubstring.startsWith("%"); + } + + /** + * Gets data from the address represented by a format substring such as %C. Will throw an + * exception if no field can be found. + */ + private static AddressField getFieldForFormatSubstring(String formatSubstring) { + return AddressField.of(formatSubstring.charAt(1)); + } + + /** + * Returns true if the address has any data for this address field. + */ + private static boolean addressHasValueForField(AddressData address, AddressField field) { + if (field == AddressField.STREET_ADDRESS) { + return address.getAddressLines().size() > 0; + } else { + String value = address.getFieldValue(field); + return (value != null && !value.isEmpty()); + } + } + + private void applyFieldOrderOverrides(String regionCode, List<AddressField> fieldOrder) { + List<AddressField> customFieldOrder = formOptions.getCustomFieldOrder(regionCode); + if (customFieldOrder == null) { + return; + } + + // We can assert that fieldOrder and customFieldOrder contain no duplicates. + // We know this by the construction above and in FormOptions but we still have to think + // about fields in the custom ordering which aren't visible (the loop below will fail if + // a non-visible field appears in the custom ordering). However in that case it's safe to + // just ignore the extraneous field. + Set<AddressField> nonVisibleCustomFields = EnumSet.copyOf(customFieldOrder); + nonVisibleCustomFields.removeAll(fieldOrder); + if (nonVisibleCustomFields.size() > 0) { + // Local mutable copy to remove non visible fields - this shouldn't happen often. + customFieldOrder = new ArrayList<AddressField>(customFieldOrder); + customFieldOrder.removeAll(nonVisibleCustomFields); + } + // It is vital for this loop to work correctly that every element in customFieldOrder + // appears in fieldOrder exactly once. + for (int fieldIdx = 0, customIdx = 0; fieldIdx < fieldOrder.size(); fieldIdx++) { + if (customFieldOrder.contains(fieldOrder.get(fieldIdx))) { + fieldOrder.set(fieldIdx, customFieldOrder.get(customIdx++)); + } + } + } + + /** + * Returns the fields that are required to be filled in for this country. This is based upon the + * "required" field in RegionDataConstants for {@code regionCode}, and handles falling back to + * the default data if necessary. + */ + static Set<AddressField> getRequiredFields(String regionCode) { + Util.checkNotNull(regionCode); + String requireString = getRequiredString(regionCode); + return getRequiredFields(requireString, regionCode); + } + + static Set<AddressField> getRequiredFields(String requireString, String regionCode) { + EnumSet<AddressField> required = EnumSet.of(AddressField.COUNTRY); + for (char c : requireString.toCharArray()) { + required.add(AddressField.of(c)); + } + return required; + } + + private static String getRequiredString(String regionCode) { + String required = getJsonValue(regionCode, AddressDataKey.REQUIRE); + if (required == null) { + required = getJsonValue("ZZ", AddressDataKey.REQUIRE); + } + return required; + } + + /** + * Returns the field width override for the specified country, or null if there's none. This is + * based upon the "width_overrides" field in RegionDataConstants for {@code regionCode}. + */ + static WidthType getWidthOverride(AddressField field, String regionCode) { + return getWidthOverride(field, regionCode, RegionDataConstants.getCountryFormatMap()); + } + + /** + * Visible for Testing - same as {@link #getWidthOverride(AddressField, String)} but testable with + * fake data. + */ + static WidthType getWidthOverride( + AddressField field, String regionCode, Map<String, String> regionDataMap) { + Util.checkNotNull(regionCode); + String overridesString = + getJsonValue(regionCode, AddressDataKey.WIDTH_OVERRIDES, regionDataMap); + if (overridesString == null || overridesString.isEmpty()) { + return null; + } + + // The field width overrides string starts with a %, so we skip the first one. + // Example string: "%C:L%S:S" which is a repeated string of + // '<%> field_character <:> width_character'. + for (int pos = 0; pos != -1;) { + int keyStartIndex = pos + 1; + int valueStartIndex = overridesString.indexOf(':', keyStartIndex + 1) + 1; + if (valueStartIndex == 0 || valueStartIndex == overridesString.length()) { + // Malformed string -- % not followed by ':' or trailing ':' + return null; + } + // Prepare for next iteration. + pos = overridesString.indexOf('%', valueStartIndex + 1); + if (valueStartIndex != keyStartIndex + 2 || + overridesString.charAt(keyStartIndex) != field.getChar()) { + // Key is not a high level field (unhandled by this code) or does not match. + // Also catches malformed string where key is of zero length (skip, not error). + continue; + } + int valueLength = (pos != -1 ? pos : overridesString.length()) - valueStartIndex; + if (valueLength != 1) { + // Malformed string -- value has length other than 1 + return null; + } + return WidthType.of(overridesString.charAt(valueStartIndex)); + } + + return null; + } + + /** + * Gets formatted address. For example, + * + * <p> John Doe</br> + * Dnar Corp</br> + * 5th St</br> + * Santa Monica CA 90123 </p> + * + * This method does not validate addresses. Also, it will "normalize" the result strings by + * removing redundant spaces and empty lines. + */ + public List<String> getEnvelopeAddress(AddressData address) { + Util.checkNotNull(address, "null input address not allowed"); + String regionCode = address.getPostalCountry(); + + String lc = address.getLanguageCode(); + ScriptType scriptType = ScriptType.LOCAL; + if (lc != null) { + scriptType = Util.isExplicitLatinScript(lc) ? ScriptType.LATIN : ScriptType.LOCAL; + } + + List<String> prunedFormat = new ArrayList<String>(); + String formatString = getFormatString(scriptType, regionCode); + List<String> formatSubstrings = getFormatSubstrings(formatString); + for (int i = 0; i < formatSubstrings.size(); i++) { + String formatSubstring = formatSubstrings.get(i); + // Always keep the newlines. + if (formatSubstring.equals(NEW_LINE)) { + prunedFormat.add(NEW_LINE); + } else if (formatSubstringRepresentsField(formatSubstring)) { + // Always keep the non-empty address fields. + if (addressHasValueForField(address, getFieldForFormatSubstring(formatSubstring))) { + prunedFormat.add(formatSubstring); + } + } else if ( + // Only keep literals that satisfy these 2 conditions: + // (1) Not preceding an empty field. + (i == formatSubstrings.size() - 1 || formatSubstrings.get(i + 1).equals(NEW_LINE) + || addressHasValueForField(address, getFieldForFormatSubstring( + formatSubstrings.get(i + 1)))) + // (2) Not following a removed field. + && (i == 0 || !formatSubstringRepresentsField(formatSubstrings.get(i - 1)) + || (!prunedFormat.isEmpty() + && formatSubstringRepresentsField(prunedFormat.get(prunedFormat.size() - 1))))) { + prunedFormat.add(formatSubstring); + } + } + + List<String> lines = new ArrayList<>(); + StringBuilder currentLine = new StringBuilder(); + for (String formatSubstring : prunedFormat) { + if (formatSubstring.equals(NEW_LINE)) { + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + currentLine.setLength(0); + } + } else if (formatSubstringRepresentsField(formatSubstring)) { + switch (getFieldForFormatSubstring(formatSubstring)) { + case STREET_ADDRESS: + // The field "street address" represents the street address lines of an address, so + // there can be multiple values. + List<String> addressLines = address.getAddressLines(); + if (addressLines.size() > 0) { + currentLine.append(addressLines.get(0)); + if (addressLines.size() > 1) { + lines.add(currentLine.toString()); + currentLine.setLength(0); + lines.addAll(addressLines.subList(1, addressLines.size())); + } + } + break; + case COUNTRY: + // Country name is treated separately. + break; + case ADMIN_AREA: + currentLine.append(address.getAdministrativeArea()); + break; + case LOCALITY: + currentLine.append(address.getLocality()); + break; + case DEPENDENT_LOCALITY: + currentLine.append(address.getDependentLocality()); + break; + case RECIPIENT: + currentLine.append(address.getRecipient()); + break; + case ORGANIZATION: + currentLine.append(address.getOrganization()); + break; + case POSTAL_CODE: + currentLine.append(address.getPostalCode()); + break; + case SORTING_CODE: + currentLine.append(address.getSortingCode()); + break; + default: + break; + } + } else { + // Not a symbol we recognise, so must be a literal. We append it unchanged. + currentLine.append(formatSubstring); + } + } + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + } + return lines; + } + + /** + * Tokenizes the format string and returns the token string list. "%" is treated as an escape + * character. For example, "%n%a%nxyz" will be split into "%n", "%a", "%n", "xyz". + * Escaped tokens correspond to either new line or address fields. The output of this method + * may contain duplicates. + */ + // TODO: Create a common method which does field parsing in one place (there are about 4 other + // places in this library where format strings are parsed). + private List<String> getFormatSubstrings(String formatString) { + List<String> parts = new ArrayList<String>(); + + boolean escaped = false; + StringBuilder currentLiteral = new StringBuilder(); + for (char c : formatString.toCharArray()) { + if (escaped) { + escaped = false; + parts.add("%" + c); + } else if (c == '%') { + if (currentLiteral.length() > 0) { + parts.add(currentLiteral.toString()); + currentLiteral.setLength(0); + } + escaped = true; + } else { + currentLiteral.append(c); + } + } + if (currentLiteral.length() > 0) { + parts.add(currentLiteral.toString()); + } + return parts; + } + + private static String getFormatString(ScriptType scriptType, String regionCode) { + String format = (scriptType == ScriptType.LOCAL) + ? getJsonValue(regionCode, AddressDataKey.FMT) + : getJsonValue(regionCode, AddressDataKey.LFMT); + if (format == null) { + format = getJsonValue("ZZ", AddressDataKey.FMT); + } + return format; + } + + private static String getJsonValue(String regionCode, AddressDataKey key) { + return getJsonValue(regionCode, key, RegionDataConstants.getCountryFormatMap()); + } + + /** + * Visible for testing only. + */ + static String getJsonValue( + String regionCode, AddressDataKey key, Map<String, String> regionDataMap) { + Util.checkNotNull(regionCode); + String jsonString = regionDataMap.get(regionCode); + Util.checkNotNull(jsonString, "no json data for region code " + regionCode); + + try { + JSONObject jsonObj = new JSONObject(new JSONTokener(jsonString)); + if (!jsonObj.has(Util.toLowerCaseLocaleIndependent(key.name()))) { + // Key not found. Return null. + return null; + } + // Gets the string for this key. + String parsedJsonString = jsonObj.getString(Util.toLowerCaseLocaleIndependent(key.name())); + return parsedJsonString; + } catch (JSONException e) { + throw new RuntimeException("Invalid json for region code " + regionCode + ": " + jsonString); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java new file mode 100644 index 00000000000..cc5a2cffec7 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Compatibility methods on top of the JSON data. + */ +public final class JsoMap extends JSONObject { + /** + * Construct a JsoMap object given some json text. This method directly evaluates the String + * that you pass in; no error or safety checking is performed, so be very careful about the + * source of your data. + * + * @param json JSON text describing an address format + * @return a JsoMap object made from the supplied JSON. + */ + public static JsoMap buildJsoMap(String json) throws JSONException { + return new JsoMap(new JSONTokener(json)); + } + + /** + * Construct an empty JsoMap. + * + * @return the empty object. + */ + static JsoMap createEmptyJsoMap() { + return new JsoMap(); + } + + /** + * constructor. + */ + protected JsoMap() { + } + + private JsoMap(JSONTokener readFrom) throws JSONException { + super(readFrom); + } + + private JsoMap(JSONObject copyFrom, String[] names) throws JSONException { + super(copyFrom, names); + } + + /** + * Remove the specified key. + * + * @param key key name. + */ + void delKey(String key) { + super.remove(key); + } + + /** + * Retrieve the string value for specified key. + * + * @param key key name. + * @return string value. + * @throws ClassCastException, IllegalArgumentException. + */ + @Override + public String get(String key) { + try { + Object o = super.get(key); + if (o instanceof String) { + return (String) o; + } else if (o instanceof Integer) { + throw new IllegalArgumentException(); + } else { + throw new ClassCastException(); + } + } catch (JSONException e) { + return null; + } + } + + /** + * Access JSONObject.get(String) which is shadowed by JsoMap.get(String). + * + * @param name A key string. + * @return The object associated with the key. + * @throws JSONException if the key is not found. + */ + public Object getObject(String name) throws JSONException { + return super.get(name); + } + + /** + * Retrieves the integer value for specified key. + * + * @return integer value or -1 if value is undefined. + */ + @Override + public int getInt(String key) { + try { + Object o = super.get(key); + if (o instanceof Integer) { + return ((Integer) o).intValue(); + } else { + throw new RuntimeException("Something other than an int was returned"); + } + } catch (JSONException e) { + return -1; + } + } + + /** + * Collect all the keys and return as a JSONArray. + * + * @return A JSONArray that contains all the keys. + */ + JSONArray getKeys() { + // names() returns null if the array was empty! + JSONArray names = super.names(); + return names != null ? names : new JSONArray(); + } + + /** + * Retrieve the JsoMap object for specified key. + * + * @param key key name. + * @return JsoMap object. + * @throws ClassCastException, IllegalArgumentException. + */ + @SuppressWarnings("unchecked") + // JSONObject.keys() has no type information. + JsoMap getObj(String key) throws ClassCastException, IllegalArgumentException { + try { + Object o = super.get(key); + if (o instanceof JSONObject) { + JSONObject value = (JSONObject) o; + ArrayList<String> keys = new ArrayList<String>(value.length()); + for (Iterator<String> it = value.keys(); it.hasNext(); ) { + keys.add(it.next()); + } + String[] names = new String[keys.size()]; + return new JsoMap(value, keys.toArray(names)); + } else if (o instanceof Integer) { + throw new IllegalArgumentException(); + } else { + throw new ClassCastException(); + } + } catch (JSONException e) { + return null; + } + } + + /** + * Check if the object has specified key. + * + * @param key The key name to be checked. + * @return true if key can be found. + */ + boolean containsKey(String key) { + return super.has(key); + } + + /** + * Merge those keys not found in this object from specified object. + * + * @param obj The other object to be merged. + */ + void mergeData(JsoMap obj) { + if (obj == null) { + return; + } + + JSONArray names = obj.names(); + if (names == null) { + return; + } + + for (int i = 0; i < names.length(); i++) { + try { + String name = names.getString(i); + try { + if (!super.has(name)) { + super.put(name, obj.getObject(name)); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } catch (JSONException e) { + // Ignored. + } + } + } + + /** + * Save a string to string mapping into this map. + * + * @param key the string key. + * @param value the String value. + */ + void put(String key, String value) { + try { + super.put(key, value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + /** + * Save a string to integer mapping into this map. + * + * @param key the string key. + * @param value the integer value. + */ + void putInt(String key, int value) { + try { + super.put(key, value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + /** + * Save a string to JSONObject mapping into this map. + * + * @param key the string key. + * @param value a JSONObject as value. + */ + void putObj(String key, JSONObject value) { + try { + super.put(key, value); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + String string() throws ClassCastException, IllegalArgumentException { + StringBuilder sb = new StringBuilder("JsoMap[\n"); + JSONArray keys = getKeys(); + for (int i = 0; i < keys.length(); i++) { + String key; + try { + key = keys.getString(i); + } catch (JSONException e) { + throw new RuntimeException(e); + } + sb.append('(').append(key).append(':').append(get(key)).append(")\n"); + } + sb.append(']'); + return sb.toString(); + } + + String map() throws ClassCastException, IllegalArgumentException { + StringBuilder sb = new StringBuilder("JsoMap[\n"); + JSONArray keys = getKeys(); + for (int i = 0; i < keys.length(); i++) { + String key; + try { + key = keys.getString(i); + } catch (JSONException e) { + throw new RuntimeException(e); + } + sb.append('(').append(key).append(':').append(getObj(key).string()).append(")\n"); + } + sb.append(']'); + return sb.toString(); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/LookupKey.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/LookupKey.java new file mode 100644 index 00000000000..2e1c397d750 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/LookupKey.java @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; + +/** + * A builder for creating keys that are used to lookup data in the local cache and fetch data from + * the server. There are two key types: {@link KeyType#DATA} or {@link KeyType#EXAMPLES}. + * <p> + * The {@link KeyType#DATA} key is built based on a universal Address hierarchy, which is:<br> + * {@link AddressField#COUNTRY} -> {@link AddressField#ADMIN_AREA} -> {@link AddressField#LOCALITY} + * -> {@link AddressField#DEPENDENT_LOCALITY} + * <p> + * The {@link KeyType#EXAMPLES} key is built with the following format:<br> + * {@link AddressField#COUNTRY} -> {@link ScriptType} -> language. </p> + */ +public final class LookupKey { + /** + * Key types. Address Widget organizes address info based on key types. For example, if you want + * to know how to verify or format an US address, you need to use {@link KeyType#DATA} to get + * that info; if you want to get an example address, you use {@link KeyType#EXAMPLES} instead. + */ + public enum KeyType { + /** + * Key type for getting address data. + */ + DATA, + /** + * Key type for getting examples. + */ + EXAMPLES + } + + /** + * Script types. This is used for countries that do not use Latin script, but accept it for + * transcribing their addresses. For example, you can write a Japanese address in Latin script + * instead of Japanese: + * <pre>7-2, Marunouchi 2-Chome, Chiyoda-ku, Tokyo 100-8799 </pre> + * <p> + * Notice that {@link ScriptType} is based on country/region, not language. + */ + public enum ScriptType { + /** + * The script that uses Roman characters like ABC (as opposed to scripts like Cyrillic or + * Arabic). + */ + LATIN, + + /** + * Local scripts. For Japan, it's Japanese (including Hiragana, Katagana, and Kanji); For + * Saudi Arabia, it's Arabic. Notice that for US, the local script is actually Latin script + * (The same goes for other countries that use Latin script). For these countries, we do not + * provide two set of data (Latin and local) since they use only Latin script. You have to + * specify the {@link ScriptType} as local instead Latin. + */ + LOCAL + } + + /** + * The universal address hierarchy. Notice that sub-administrative area is neglected here since + * it is not required to fill out address forms. + */ + private static final AddressField[] HIERARCHY = { + AddressField.COUNTRY, + AddressField.ADMIN_AREA, + AddressField.LOCALITY, + AddressField.DEPENDENT_LOCALITY}; + + private static final String SLASH_DELIM = "/"; + + private static final String DASH_DELIM = "--"; + + private static final String DEFAULT_LANGUAGE = "_default"; + + private final KeyType keyType; + + private final ScriptType scriptType; + + // Values for each address field in the hierarchy. + private final Map<AddressField, String> nodes; + + private final String keyString; + + private final String languageCode; + + private LookupKey(Builder builder) { + this.keyType = builder.keyType; + this.scriptType = builder.script; + this.nodes = builder.nodes; + this.languageCode = builder.languageCode; + this.keyString = createKeyString(); + } + + /** + * Gets a lookup key built from the values of nodes in the hierarchy up to and including the input + * address field. This method does not allow keys with a key type of {@link KeyType#EXAMPLES}. + * + * @param field a field in the address hierarchy. + * @return key of the specified address field. If address field is not in the hierarchy, or is + * more granular than the data present in the current key, returns null. For example, + * if your current key is "data/US" (down to COUNTRY level), and you want to get the key + * for LOCALITY (more granular than COUNTRY), it will return null. + */ + public LookupKey getKeyForUpperLevelField(AddressField field) { + if (keyType != KeyType.DATA) { + // We only support getting the parent key for the data key type. + throw new RuntimeException("Only support getting parent keys for the data key type."); + } + Builder newKeyBuilder = new Builder(this); + + boolean removeNode = false; + boolean fieldInHierarchy = false; + for (AddressField hierarchyField : HIERARCHY) { + if (removeNode) { + if (newKeyBuilder.nodes.containsKey(hierarchyField)) { + newKeyBuilder.nodes.remove(hierarchyField); + } + } + if (hierarchyField == field) { + if (!newKeyBuilder.nodes.containsKey(hierarchyField)) { + return null; + } + removeNode = true; + fieldInHierarchy = true; + } + } + + if (!fieldInHierarchy) { + return null; + } + + newKeyBuilder.languageCode = languageCode; + newKeyBuilder.script = scriptType; + + return newKeyBuilder.build(); + } + + /** + * Returns the string value of a field in a key for a particular + * AddressField. For example, for the key "data/US/CA" and the address + * field AddressField.COUNTRY, "US" would be returned. Returns an empty + * string if the key does not have this field in it. + */ + String getValueForUpperLevelField(AddressField field) { + if (!this.nodes.containsKey(field)) { + return ""; + } + + return this.nodes.get(field); + } + + /** + * Gets parent key for data key. For example, parent key for "data/US/CA" is "data/US". This + * method does not allow key with key type of {@link KeyType#EXAMPLES}. + */ + LookupKey getParentKey() { + if (keyType != KeyType.DATA) { + throw new RuntimeException("Only support getting parent keys for the data key type."); + } + // Root key's parent should be null. + if (!nodes.containsKey(AddressField.COUNTRY)) { + return null; + } + + Builder parentKeyBuilder = new Builder(this); + AddressField mostGranularField = AddressField.COUNTRY; + + for (AddressField hierarchyField : HIERARCHY) { + if (!nodes.containsKey(hierarchyField)) { + break; + } + mostGranularField = hierarchyField; + } + parentKeyBuilder.nodes.remove(mostGranularField); + return parentKeyBuilder.build(); + } + + public KeyType getKeyType() { + return keyType; + } + + /** + * Creates the string format of the given key. E.g., "data/US/CA". + */ + private String createKeyString() { + StringBuilder keyBuilder = new StringBuilder(Util.toLowerCaseLocaleIndependent(keyType.name())); + + if (keyType == KeyType.DATA) { + for (AddressField field : HIERARCHY) { + if (!nodes.containsKey(field)) { + break; + } + keyBuilder.append(SLASH_DELIM).append(nodes.get(field)); + } + // Only append the language if this is not the root key and there was a language. + if (languageCode != null && nodes.size() > 0) { + keyBuilder.append(DASH_DELIM).append(languageCode); + } + } else { + if (nodes.containsKey(AddressField.COUNTRY)) { + // Example key. E.g., "examples/TW/local/_default". + keyBuilder.append(SLASH_DELIM) + .append(nodes.get(AddressField.COUNTRY)) + .append(SLASH_DELIM) + .append(Util.toLowerCaseLocaleIndependent(scriptType.name())) + .append(SLASH_DELIM) + .append(DEFAULT_LANGUAGE); + } + } + + return keyBuilder.toString(); + } + + /** + * Gets a lookup key as a plain text string., e.g., "data/US/CA". + */ + @Override + public String toString() { + return keyString; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (obj.getClass() != this.getClass())) { + return false; + } + + return ((LookupKey) obj).toString().equals(keyString); + } + + @Override + public int hashCode() { + return keyString.hashCode(); + } + + static boolean hasValidKeyPrefix(String key) { + for (KeyType type : KeyType.values()) { + if (key.startsWith(Util.toLowerCaseLocaleIndependent(type.name()))) { + return true; + } + } + return false; + } + + /** + * Builds lookup keys. + */ + // TODO: This is used in AddressWidget in a small number of places and it should be possible + // to hide this type within this package quite easily. + public static class Builder { + private KeyType keyType; + + // Default to LOCAL script. + private ScriptType script = ScriptType.LOCAL; + + private final Map<AddressField, String> nodes = + new EnumMap<AddressField, String>(AddressField.class); + + private String languageCode; + + /** + * Creates a new builder for the specified key type. keyType cannot be null. + */ + public Builder(KeyType keyType) { + this.keyType = keyType; + } + + /** + * Creates a new builder for the specified key. oldKey cannot be null. + */ + Builder(LookupKey oldKey) { + this.keyType = oldKey.keyType; + this.script = oldKey.scriptType; + this.languageCode = oldKey.languageCode; + for (AddressField field : HIERARCHY) { + if (!oldKey.nodes.containsKey(field)) { + break; + } + this.nodes.put(field, oldKey.nodes.get(field)); + } + } + + /** + * Builds the {@link LookupKey} with the input key string. Input string has to represent + * either a {@link KeyType#DATA} key or a {@link KeyType#EXAMPLES} key. Also, key hierarchy + * deeper than {@link AddressField#DEPENDENT_LOCALITY} is not allowed. Notice that if any + * node in the hierarchy is empty, all the descendant nodes' values will be neglected. For + * example, input string "data/US//Mt View" will become "data/US". + * + * @param keyString e.g., "data/US/CA" + */ + public Builder(String keyString) { + String[] parts = keyString.split(SLASH_DELIM); + + if (!parts[0].equals(Util.toLowerCaseLocaleIndependent(KeyType.DATA.name())) + && !parts[0].equals(Util.toLowerCaseLocaleIndependent(KeyType.EXAMPLES.name()))) { + throw new RuntimeException("Wrong key type: " + parts[0]); + } + if (parts.length > HIERARCHY.length + 1) { + // Assume that any extra elements found in the key belong in the 'dependent locality' field. + // This means that a key string of /EXAMPLES/C/A/L/D/E would result in a dependent locality + // value of 'D/E'. This also means that if it's the actual locality name has a slash in it + // (for example 'L/D'), the locality field which we break down will be incorrect + // (for example: 'L'). Regardless, the actual breakdown of the key doesn't impact the server + // lookup, so there will be no problems. + String[] extraParts = Arrays.copyOfRange(parts, HIERARCHY.length + 1, parts.length + 1); + + // Update the original array to only contain the number of elements which we expect. + parts = Arrays.copyOfRange(parts, 0, HIERARCHY.length + 1); + + // Append the extra parts to the last element (dependent locality). + for (String element : extraParts) { + if (element != null) { + parts[4] += SLASH_DELIM + element; + } + } + } + + if (parts[0].equals("data")) { + keyType = KeyType.DATA; + + // Process all parts of the key, starting from the country. + for (int i = 1; i < parts.length; i++) { + // TODO: We shouldn't need the trimToNull here. + String substr = Util.trimToNull(parts[i]); + if (substr == null) { + break; + } + // If a language code specification was present, extract this. This should only be there + // (if it ever is) on the last node. + if (substr.contains(DASH_DELIM)) { + String[] s = substr.split(DASH_DELIM); + if (s.length != 2) { + throw new RuntimeException( + "Wrong format: Substring should be <last node value>--<language code>"); + } + substr = s[0]; + languageCode = s[1]; + } + + this.nodes.put(HIERARCHY[i - 1], substr); + } + } else if (parts[0].equals("examples")) { + keyType = KeyType.EXAMPLES; + + // Parses country info. + if (parts.length > 1) { + this.nodes.put(AddressField.COUNTRY, parts[1]); + } + + // Parses script types. + if (parts.length > 2) { + String scriptStr = parts[2]; + if (scriptStr.equals("local")) { + this.script = ScriptType.LOCAL; + } else if (scriptStr.equals("latin")) { + this.script = ScriptType.LATIN; + } else { + throw new RuntimeException("Script type has to be either latin or local."); + } + } + + // Parses language code. Example: "zh_Hant" in + // "examples/TW/local/zH_Hant". + if (parts.length > 3 && !parts[3].equals(DEFAULT_LANGUAGE)) { + languageCode = parts[3]; + } + } + } + + Builder setLanguageCode(String languageCode) { + this.languageCode = languageCode; + return this; + } + + /** + * Sets key using {@link AddressData}. Notice that if any node in the hierarchy is empty, + * all the descendant nodes' values will be neglected. For example, the following address + * misses {@link AddressField#ADMIN_AREA}, thus its data key will be "data/US". + * + * <p> country: US<br> administrative area: null<br> locality: Mt. View </p> + */ + public Builder setAddressData(AddressData data) { + languageCode = data.getLanguageCode(); + if (languageCode != null) { + if (Util.isExplicitLatinScript(languageCode)) { + script = ScriptType.LATIN; + } + } + + if (data.getPostalCountry() == null) { + return this; + } + this.nodes.put(AddressField.COUNTRY, data.getPostalCountry()); + + if (data.getAdministrativeArea() == null) { + return this; + } + this.nodes.put(AddressField.ADMIN_AREA, data.getAdministrativeArea()); + + if (data.getLocality() == null) { + return this; + } + this.nodes.put(AddressField.LOCALITY, data.getLocality()); + + if (data.getDependentLocality() == null) { + return this; + } + this.nodes.put(AddressField.DEPENDENT_LOCALITY, data.getDependentLocality()); + return this; + } + + public LookupKey build() { + return new LookupKey(this); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/NotifyingListener.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/NotifyingListener.java new file mode 100644 index 00000000000..9ca8643b99c --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/NotifyingListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * A helper class to let the calling thread wait until loading has finished. + */ +// TODO: Consider dealing with interruption in a more recoverable way. +public final class NotifyingListener implements DataLoadListener { + private boolean done = false; + + @Override + public void dataLoadingBegin() { + } + + @Override + public synchronized void dataLoadingEnd() { + done = true; + notifyAll(); + } + + /** + * Waits for a call to {@link #dataLoadingEnd} to have occurred. If this thread is interrupted, + * the {@code InterruptedException} is propagated immediately and the loading may not yet have + * finished. This leaves callers in a potentially unrecoverable state. + */ + public synchronized void waitLoadingEnd() throws InterruptedException { + while (!done) { + wait(); + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java new file mode 100644 index 00000000000..b5b91634f6f --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * If autocomplete is enabled on the AddressWidget, setting an OnAddressSelectedListener + * will cause onAddressSelected to be called when the user clicks on an autocomplete + * suggestion in the dropdown list. + */ +public interface OnAddressSelectedListener { + void onAddressSelected(AddressData addressData); +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionData.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionData.java new file mode 100644 index 00000000000..d03320f0f51 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionData.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * A simple class to hold region data. + */ +// This class used to purport to be immutable, but it is no such thing. +// TODO: Make this class actually immutable and not just pretending to be immutable. +public final class RegionData { + private String key; + private String name; + + /** + * Create a new RegionData object. + */ + private RegionData() { + } + + /** + * Copy constructor. data should not be null. + * + * @param data A populated instance of RegionData + */ + private RegionData(RegionData data) { + Util.checkNotNull(data); + this.key = data.key; + this.name = data.name; + } + + /** + * Gets the key of the region. For example, California's key is "CA". + */ + public String getKey() { + return key; + } + + /** + * Gets the name. Returns null if not specified. + */ + String getName() { + return name; + } + + /** + * Gets the best display name. Returns the name if this is not null, otherwise the key. + */ + public String getDisplayName() { + return (name != null) ? name : key; + } + + /** + * Checks if the input subkey is the name (in Latin or local script) of the region. Returns + * false if subkey is not a valid name for the region, or the input subkey is null. + * + * @param subkey a string that refers to the name of a geo location. Like "California", "CA", or + * "Mountain View". Names in the local script are also supported. + */ + public boolean isValidName(String subkey) { + if (subkey == null) { + return false; + } + if (subkey.equalsIgnoreCase(key) || subkey.equalsIgnoreCase(name)) { + return true; + } + return false; + } + + /** + * A builder class to facilitate the creation of RegionData objects. + */ + // TODO: Replace this broken builder implementation with a simple static factory method. + public static class Builder { + RegionData data = new RegionData(); + + public RegionData build() { + return new RegionData(data); + } + + public Builder setKey(String key) { + Util.checkNotNull(key, "Key should not be null."); + data.key = key; + return this; + } + + /** + * Sets name of the region. For example, "California". If the name is an empty string, sets + * it to null. + */ + public Builder setName(String name) { + data.name = Util.trimToNull(name); + return this; + } + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java new file mode 100644 index 00000000000..fce8747b51a --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java @@ -0,0 +1,285 @@ +// Copyright (C) 2010 Google Inc. +// +// 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.google.i18n.addressinput.common; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +public final class RegionDataConstants { + private static final Map<String, String> addressDataMap = createMap(); + + public static Map<String, String> getCountryFormatMap() { + return addressDataMap; + } + + private static Map<String, String> createMap() { + TreeMap<String, String> map = new TreeMap<String, String>(); + map.put("AC", "{\"name\":\"ASCENSION ISLAND\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("AD", "{\"name\":\"ANDORRA\",\"lang\":\"ca\",\"languages\":\"ca\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("AE", "{\"name\":\"UNITED ARAB EMIRATES\",\"lang\":\"ar\",\"languages\":\"ar\",\"lfmt\":\"%N%n%O%n%A%n%S\",\"fmt\":\"%N%n%O%n%A%n%S\",\"require\":\"AS\",\"state_name_type\":\"emirate\"}"); + map.put("AF", "{\"name\":\"AFGHANISTAN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("AG", "{\"name\":\"ANTIGUA AND BARBUDA\",\"require\":\"A\"}"); + map.put("AI", "{\"name\":\"ANGUILLA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("AL", "{\"name\":\"ALBANIA\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C\"}"); + map.put("AM", "{\"name\":\"ARMENIA\",\"lang\":\"hy\",\"languages\":\"hy\",\"lfmt\":\"%N%n%O%n%A%n%Z%n%C%n%S\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C%n%S\"}"); + map.put("AO", "{\"name\":\"ANGOLA\"}"); + map.put("AQ", "{\"name\":\"ANTARCTICA\"}"); + map.put("AR", "{\"name\":\"ARGENTINA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\",\"upper\":\"ACZ\"}"); + map.put("AS", "{\"name\":\"AMERICAN SAMOA\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("AT", "{\"name\":\"AUSTRIA\",\"fmt\":\"%O%n%N%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("AU", "{\"name\":\"AUSTRALIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"locality_name_type\":\"suburb\",\"state_name_type\":\"state\"}"); + map.put("AW", "{\"name\":\"ARUBA\"}"); + map.put("AX", "{\"name\":\"FINLAND\",\"fmt\":\"%O%n%N%n%A%nAX-%Z %C%nÅLAND\",\"require\":\"ACZ\",\"postprefix\":\"AX-\"}"); + map.put("AZ", "{\"name\":\"AZERBAIJAN\",\"fmt\":\"%N%n%O%n%A%nAZ %Z %C\",\"postprefix\":\"AZ \"}"); + map.put("BA", "{\"name\":\"BOSNIA AND HERZEGOVINA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("BB", "{\"name\":\"BARBADOS\",\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"state_name_type\":\"parish\"}"); + map.put("BD", "{\"name\":\"BANGLADESH\",\"fmt\":\"%N%n%O%n%A%n%C - %Z\"}"); + map.put("BE", "{\"name\":\"BELGIUM\",\"fmt\":\"%O%n%N%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("BF", "{\"name\":\"BURKINA FASO\",\"fmt\":\"%N%n%O%n%A%n%C %X\"}"); + map.put("BG", "{\"name\":\"BULGARIA (REP.)\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("BH", "{\"name\":\"BAHRAIN\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("BI", "{\"name\":\"BURUNDI\"}"); + map.put("BJ", "{\"name\":\"BENIN\",\"upper\":\"AC\"}"); + map.put("BL", "{\"name\":\"SAINT BARTHELEMY\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("BM", "{\"name\":\"BERMUDA\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("BN", "{\"name\":\"BRUNEI DARUSSALAM\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("BO", "{\"name\":\"BOLIVIA\",\"upper\":\"AC\"}"); + map.put("BQ", "{\"name\":\"BONAIRE, SINT EUSTATIUS, AND SABA\"}"); + map.put("BR", "{\"name\":\"BRAZIL\",\"lang\":\"pt\",\"languages\":\"pt\",\"fmt\":\"%O%n%N%n%A%n%D%n%C-%S%n%Z\",\"require\":\"ASCZ\",\"upper\":\"CS\",\"sublocality_name_type\":\"neighborhood\",\"state_name_type\":\"state\",\"width_overrides\":\"%C:L%S:S\",\"label_overrides\":[{\"field\":\"S2\",\"label\":\"Setor/ADE/Folha\"},{\"field\":\"S3\",\"label\":\"Quadra\"},{\"field\":\"S4\",\"label\":\"Trecho/AE/Modulo\"},{\"field\":\"S5\",\"label\":\"Cj/Bl/MI/Projeção/Etapa\"},{\"field\":\"LP\",\"label\":\"Lote\"},{\"field\":\"BI\",\"label\":\"Casa/Comercio\"},{\"field\":\"CG\",\"label\":\"Complexo/Chácara\"}]}"); + map.put("BS", "{\"name\":\"BAHAMAS\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C, %S\",\"state_name_type\":\"island\"}"); + map.put("BT", "{\"name\":\"BHUTAN\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("BV", "{\"name\":\"BOUVET ISLAND\"}"); + map.put("BW", "{\"name\":\"BOTSWANA\"}"); + map.put("BY", "{\"name\":\"BELARUS\",\"fmt\":\"%S%n%Z %C%n%A%n%O%n%N\"}"); + map.put("BZ", "{\"name\":\"BELIZE\"}"); + map.put("CA", "{\"name\":\"CANADA\",\"lang\":\"en\",\"languages\":\"en~fr\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOSZ\"}"); + map.put("CC", "{\"name\":\"COCOS (KEELING) ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}"); + map.put("CD", "{\"name\":\"CONGO (DEM. REP.)\"}"); + map.put("CF", "{\"name\":\"CENTRAL AFRICAN REPUBLIC\"}"); + map.put("CG", "{\"name\":\"CONGO (REP.)\"}"); + map.put("CH", "{\"name\":\"SWITZERLAND\",\"fmt\":\"%O%n%N%n%A%nCH-%Z %C\",\"require\":\"ACZ\",\"upper\":\"\",\"postprefix\":\"CH-\"}"); + map.put("CI", "{\"name\":\"COTE D'IVOIRE\",\"fmt\":\"%N%n%O%n%X %A %C %X\"}"); + map.put("CK", "{\"name\":\"COOK ISLANDS\"}"); + map.put("CL", "{\"name\":\"CHILE\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\"}"); + map.put("CM", "{\"name\":\"CAMEROON\"}"); + map.put("CN", "{\"name\":\"CHINA\",\"lang\":\"zh\",\"languages\":\"zh\",\"lfmt\":\"%N%n%O%n%A%n%D%n%C%n%S, %Z\",\"fmt\":\"%Z%n%S%C%D%n%A%n%O%n%N\",\"require\":\"ACSZ\",\"upper\":\"S\",\"sublocality_name_type\":\"district\",\"width_overrides\":\"%S:S%C:S%D:S\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"市/自治州/地区/盟\",\"lang\":\"zh\"},{\"field\":\"S\",\"label\":\"省/自治区/直辖市\",\"lang\":\"zh\"},{\"field\":\"D\",\"label\":\"区/县/旗\",\"lang\":\"zh\"}]}"); + map.put("CO", "{\"name\":\"COLOMBIA\",\"fmt\":\"%N%n%O%n%A%n%C, %S, %Z\",\"require\":\"AS\",\"state_name_type\":\"department\",\"label_overrides\":[{\"field\":\"LL\",\"label\":\"Vereda\"},{\"field\":\"A3\",\"label\":\"Corregimiento\"},{\"field\":\"A2\",\"label\":\"Municipio\"}]}"); + map.put("CR", "{\"name\":\"COSTA RICA\",\"fmt\":\"%N%n%O%n%A%n%S, %C%n%Z\",\"require\":\"ACS\"}"); + map.put("CU", "{\"name\":\"CUBA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%C %S%n%Z\"}"); + map.put("CV", "{\"name\":\"CAPE VERDE\",\"lang\":\"pt\",\"languages\":\"pt\",\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\",\"state_name_type\":\"island\"}"); + map.put("CW", "{\"name\":\"CURACAO\"}"); + map.put("CX", "{\"name\":\"CHRISTMAS ISLAND\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}"); + map.put("CY", "{\"name\":\"CYPRUS\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("CZ", "{\"name\":\"CZECH REP.\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"label_overrides\":[{\"field\":\"NH\",\"label\":\"Obecní část\",\"lang\":\"cs\"},{\"field\":\"NH\",\"label\":\"Obecný časť\",\"lang\":\"sk\"},{\"field\":\"BI\",\"label\":\"Descriptive No.\"},{\"field\":\"BI\",\"label\":\"Popisné číslo\",\"lang\":\"cs\"},{\"field\":\"BI\",\"label\":\"Súpisné číslo\",\"lang\":\"sk\"},{\"field\":\"SN\",\"label\":\"Orientation No.\"},{\"field\":\"SN\",\"label\":\"Orientační číslo\",\"lang\":\"cs\"},{\"field\":\"SN\",\"label\":\"Orientačné číslo\",\"lang\":\"sk\"},{\"field\":\"S1\",\"label\":\"City District\"},{\"field\":\"S1\",\"label\":\"Městská část\",\"lang\":\"cs\"},{\"field\":\"S1\",\"label\":\"Mestská časť\",\"lang\":\"sk\"}]}"); + map.put("DE", "{\"name\":\"GERMANY\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("DJ", "{\"name\":\"DJIBOUTI\"}"); + map.put("DK", "{\"name\":\"DENMARK\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("DM", "{\"name\":\"DOMINICA\"}"); + map.put("DO", "{\"name\":\"DOMINICAN REP.\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("DZ", "{\"name\":\"ALGERIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("EC", "{\"name\":\"ECUADOR\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C\",\"upper\":\"CZ\"}"); + map.put("EE", "{\"name\":\"ESTONIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"Linn/vald\",\"lang\":\"et\"},{\"field\":\"C\",\"label\":\"City/Parish\",\"lang\":\"en\"}]}"); + map.put("EG", "{\"name\":\"EGYPT\",\"lang\":\"ar\",\"languages\":\"ar\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\"}"); + map.put("EH", "{\"name\":\"WESTERN SAHARA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("ER", "{\"name\":\"ERITREA\"}"); + map.put("ES", "{\"name\":\"SPAIN\",\"lang\":\"es\",\"languages\":\"es~ca~gl~eu\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}"); + map.put("ET", "{\"name\":\"ETHIOPIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("FI", "{\"name\":\"FINLAND\",\"fmt\":\"%O%n%N%n%A%nFI-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"FI-\"}"); + map.put("FJ", "{\"name\":\"FIJI\"}"); + map.put("FK", "{\"name\":\"FALKLAND ISLANDS (MALVINAS)\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("FM", "{\"name\":\"MICRONESIA (Federated State of)\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("FO", "{\"name\":\"FAROE ISLANDS\",\"fmt\":\"%N%n%O%n%A%nFO%Z %C\",\"postprefix\":\"FO\"}"); + map.put("FR", "{\"name\":\"FRANCE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"CX\"}"); + map.put("GA", "{\"name\":\"GABON\"}"); + map.put("GB", "{\"name\":\"UNITED KINGDOM\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\",\"locality_name_type\":\"post_town\",\"label_overrides\":[{\"field\":\"LL\",\"message\":\"MSG_DEPENDENT_LOCALITY_LABEL\"},{\"field\":\"Z\",\"label\":\"Postcode\",\"lang\":\"en\"}]}"); + map.put("GD", "{\"name\":\"GRENADA (WEST INDIES)\"}"); + map.put("GE", "{\"name\":\"GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("GF", "{\"name\":\"FRENCH GUIANA\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("GG", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%nGUERNSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("GH", "{\"name\":\"GHANA\"}"); + map.put("GI", "{\"name\":\"GIBRALTAR\",\"fmt\":\"%N%n%O%n%A%nGIBRALTAR%n%Z\",\"require\":\"A\"}"); + map.put("GL", "{\"name\":\"GREENLAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("GM", "{\"name\":\"GAMBIA\"}"); + map.put("GN", "{\"name\":\"GUINEA\",\"fmt\":\"%N%n%O%n%Z %A %C\"}"); + map.put("GP", "{\"name\":\"GUADELOUPE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("GQ", "{\"name\":\"EQUATORIAL GUINEA\"}"); + map.put("GR", "{\"name\":\"GREECE\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("GS", "{\"name\":\"SOUTH GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("GT", "{\"name\":\"GUATEMALA\",\"fmt\":\"%N%n%O%n%A%n%Z- %C\"}"); + map.put("GU", "{\"name\":\"GUAM\",\"fmt\":\"%N%n%O%n%A%n%C %Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\",\"zip_name_type\":\"zip\"}"); + map.put("GW", "{\"name\":\"GUINEA-BISSAU\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("GY", "{\"name\":\"GUYANA\"}"); + map.put("HK", "{\"name\":\"HONG KONG\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant~en\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S\",\"fmt\":\"%S%n%C%n%A%n%O%n%N\",\"require\":\"AS\",\"upper\":\"S\",\"locality_name_type\":\"district\",\"state_name_type\":\"area\",\"width_overrides\":\"%S:S%C:L\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"地区\",\"lang\":\"zh\"},{\"field\":\"C\",\"label\":\"地區\",\"lang\":\"zh-HK\"},{\"field\":\"C\",\"label\":\"地區\",\"lang\":\"zh-TW\"},{\"field\":\"CS\",\"label\":\"Flat / Room\",\"lang\":\"en\"},{\"field\":\"CS\",\"label\":\"單位編號\",\"lang\":\"zh-HK\"},{\"field\":\"BG\",\"label\":\"大廈名稱\",\"lang\":\"zh-HK\"}]}"); + map.put("HM", "{\"name\":\"HEARD AND MCDONALD ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}"); + map.put("HN", "{\"name\":\"HONDURAS\",\"fmt\":\"%N%n%O%n%A%n%C, %S%n%Z\",\"require\":\"ACS\"}"); + map.put("HR", "{\"name\":\"CROATIA\",\"fmt\":\"%N%n%O%n%A%nHR-%Z %C\",\"postprefix\":\"HR-\"}"); + map.put("HT", "{\"name\":\"HAITI\",\"fmt\":\"%N%n%O%n%A%nHT%Z %C\",\"postprefix\":\"HT\"}"); + map.put("HU", "{\"name\":\"HUNGARY (Rep.)\",\"fmt\":\"%N%n%O%n%C%n%A%n%Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\"}"); + map.put("ID", "{\"name\":\"INDONESIA\",\"lang\":\"id\",\"languages\":\"id\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"require\":\"AS\",\"label_overrides\":[{\"field\":\"A7\",\"label\":\"RT\"},{\"field\":\"A6\",\"label\":\"RW\"},{\"field\":\"A5\",\"label\":\"Dusun/Banjar\"},{\"field\":\"BI\",\"label\":\"Blok\"},{\"field\":\"A4\",\"message\":\"MSG_VILLAGE\"},{\"field\":\"A3\",\"label\":\"Kecamatan\"},{\"field\":\"S1\",\"label\":\"Pasar\"}]}"); + map.put("IE", "{\"name\":\"IRELAND\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D%n%C%n%S %Z\",\"sublocality_name_type\":\"townland\",\"state_name_type\":\"county\",\"zip_name_type\":\"eircode\",\"label_overrides\":[{\"field\":\"S\",\"label\":\"郡\",\"lang\":\"zh\"}]}"); + map.put("IL", "{\"name\":\"ISRAEL\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("IM", "{\"name\":\"ISLE OF MAN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("IN", "{\"name\":\"INDIA\",\"lang\":\"en\",\"languages\":\"en~hi\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"require\":\"ACSZ\",\"state_name_type\":\"state\",\"zip_name_type\":\"pin\",\"label_overrides\":[{\"field\":\"S1\",\"label\":\"Sublocality 1\"},{\"field\":\"S2\",\"label\":\"Sublocality 2\"},{\"field\":\"S3\",\"label\":\"Sublocality 3\"},{\"field\":\"S4\",\"label\":\"Sublocality 4\"}]}"); + map.put("IO", "{\"name\":\"BRITISH INDIAN OCEAN TERRITORY\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("IQ", "{\"name\":\"IRAQ\",\"fmt\":\"%O%n%N%n%A%n%C, %S%n%Z\",\"require\":\"ACS\",\"upper\":\"CS\"}"); + map.put("IR", "{\"name\":\"IRAN\",\"lang\":\"fa\",\"languages\":\"fa\",\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\",\"sublocality_name_type\":\"neighborhood\"}"); + map.put("IS", "{\"name\":\"ICELAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("IT", "{\"name\":\"ITALY\",\"lang\":\"it\",\"languages\":\"it\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}"); + map.put("JE", "{\"name\":\"CHANNEL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%nJERSEY%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("JM", "{\"name\":\"JAMAICA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %X\",\"require\":\"ACS\",\"state_name_type\":\"parish\"}"); + map.put("JO", "{\"name\":\"JORDAN\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("JP", "{\"name\":\"JAPAN\",\"lang\":\"ja\",\"languages\":\"ja\",\"lfmt\":\"%N%n%O%n%A, %S%n%Z\",\"fmt\":\"〒%Z%n%S%n%A%n%O%n%N\",\"require\":\"ASZ\",\"upper\":\"S\",\"state_name_type\":\"prefecture\",\"width_overrides\":\"%S:S\",\"label_overrides\":[{\"field\":\"JED\",\"label\":\"Edaban\"},{\"field\":\"JED\",\"label\":\"枝番\",\"lang\":\"ja\"},{\"field\":\"JCH\",\"label\":\"Banchi\"},{\"field\":\"JCH\",\"label\":\"番地\",\"lang\":\"ja\"},{\"field\":\"JGA\",\"label\":\"Gaiku\"},{\"field\":\"JGA\",\"label\":\"街区\",\"lang\":\"ja\"},{\"field\":\"JKO\",\"label\":\"Koaza\"},{\"field\":\"JKO\",\"label\":\"小字\",\"lang\":\"ja\"},{\"field\":\"JOO\",\"label\":\"Ōaza\"},{\"field\":\"JOO\",\"label\":\"大字\",\"lang\":\"ja\"},{\"field\":\"JSS\",\"label\":\"Ku\"},{\"field\":\"JSS\",\"label\":\"区\",\"lang\":\"ja\"},{\"field\":\"JSH\",\"label\":\"Shi\"},{\"field\":\"JSH\",\"label\":\"市\",\"lang\":\"ja\"},{\"field\":\"JGN\",\"label\":\"Gun\"},{\"field\":\"JGN\",\"label\":\"郡\",\"lang\":\"ja\"}]}"); + map.put("KE", "{\"name\":\"KENYA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("KG", "{\"name\":\"KYRGYZSTAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("KH", "{\"name\":\"CAMBODIA\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("KI", "{\"name\":\"KIRIBATI\",\"fmt\":\"%N%n%O%n%A%n%S%n%C\",\"upper\":\"ACNOS\",\"state_name_type\":\"island\"}"); + map.put("KM", "{\"name\":\"COMOROS\",\"upper\":\"AC\"}"); + map.put("KN", "{\"name\":\"SAINT KITTS AND NEVIS\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C, %S\",\"require\":\"ACS\",\"state_name_type\":\"island\"}"); + map.put("KP", "{\"name\":\"NORTH KOREA\",\"lang\":\"ko\",\"languages\":\"ko\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S, %Z\",\"fmt\":\"%Z%n%S%n%C%n%A%n%O%n%N\"}"); + map.put("KR", "{\"name\":\"SOUTH KOREA\",\"lang\":\"ko\",\"languages\":\"ko\",\"lfmt\":\"%N%n%O%n%A%n%D%n%C%n%S%n%Z\",\"fmt\":\"%S %C%D%n%A%n%O%n%N%n%Z\",\"require\":\"ACSZ\",\"upper\":\"Z\",\"sublocality_name_type\":\"district\",\"state_name_type\":\"do_si\",\"label_overrides\":[{\"field\":\"BI\",\"message\":\"MSG_STREET_NUMBER\"},{\"field\":\"S2\",\"message\":\"MSG_NEIGHBORHOOD\"},{\"field\":\"S4\",\"message\":\"MSG_STREET_NAME\"}]}"); + map.put("KW", "{\"name\":\"KUWAIT\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("KY", "{\"name\":\"CAYMAN ISLANDS\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%S %Z\",\"require\":\"AS\",\"state_name_type\":\"island\"}"); + map.put("KZ", "{\"name\":\"KAZAKHSTAN\",\"fmt\":\"%Z%n%S%n%C%n%A%n%O%n%N\"}"); + map.put("LA", "{\"name\":\"LAO (PEOPLE'S DEM. REP.)\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("LB", "{\"name\":\"LEBANON\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("LC", "{\"name\":\"SAINT LUCIA\"}"); + map.put("LI", "{\"name\":\"LIECHTENSTEIN\",\"fmt\":\"%O%n%N%n%A%nFL-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"FL-\"}"); + map.put("LK", "{\"name\":\"SRI LANKA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("LR", "{\"name\":\"LIBERIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("LS", "{\"name\":\"LESOTHO\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("LT", "{\"name\":\"LITHUANIA\",\"fmt\":\"%O%n%N%n%A%nLT-%Z %C\",\"postprefix\":\"LT-\"}"); + map.put("LU", "{\"name\":\"LUXEMBOURG\",\"fmt\":\"%O%n%N%n%A%nL-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"L-\"}"); + map.put("LV", "{\"name\":\"LATVIA\",\"fmt\":\"%N%n%O%n%A%n%C, %Z\"}"); + map.put("LY", "{\"name\":\"LIBYA\"}"); + map.put("MA", "{\"name\":\"MOROCCO\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("MC", "{\"name\":\"MONACO\",\"fmt\":\"%N%n%O%n%A%nMC-%Z %C %X\",\"postprefix\":\"MC-\"}"); + map.put("MD", "{\"name\":\"Rep. MOLDOVA\",\"fmt\":\"%N%n%O%n%A%nMD-%Z %C\",\"postprefix\":\"MD-\"}"); + map.put("ME", "{\"name\":\"MONTENEGRO\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("MF", "{\"name\":\"SAINT MARTIN\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("MG", "{\"name\":\"MADAGASCAR\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("MH", "{\"name\":\"MARSHALL ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("MK", "{\"name\":\"MACEDONIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("ML", "{\"name\":\"MALI\"}"); + map.put("MM", "{\"name\":\"MYANMAR\",\"fmt\":\"%N%n%O%n%A%n%C, %Z\"}"); + map.put("MN", "{\"name\":\"MONGOLIA\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\"}"); + map.put("MO", "{\"name\":\"MACAO\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant\",\"lfmt\":\"%N%n%O%n%A\",\"fmt\":\"%A%n%O%n%N\",\"require\":\"A\"}"); + map.put("MP", "{\"name\":\"NORTHERN MARIANA ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("MQ", "{\"name\":\"MARTINIQUE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("MR", "{\"name\":\"MAURITANIA\",\"upper\":\"AC\"}"); + map.put("MS", "{\"name\":\"MONTSERRAT\"}"); + map.put("MT", "{\"name\":\"MALTA\",\"fmt\":\"%N%n%O%n%A%n%C %Z\",\"upper\":\"CZ\"}"); + map.put("MU", "{\"name\":\"MAURITIUS\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C\",\"upper\":\"CZ\"}"); + map.put("MV", "{\"name\":\"MALDIVES\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("MW", "{\"name\":\"MALAWI\",\"fmt\":\"%N%n%O%n%A%n%C %X\"}"); + map.put("MX", "{\"name\":\"MEXICO\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%D%n%Z %C, %S\",\"require\":\"ACZ\",\"upper\":\"CSZ\",\"sublocality_name_type\":\"neighborhood\",\"state_name_type\":\"state\",\"width_overrides\":\"%S:S\",\"label_overrides\":[{\"field\":\"S1\",\"label\":\"Delegación\"},{\"field\":\"S2\",\"label\":\"Supermanzana\"},{\"field\":\"S3\",\"label\":\"Manzana\"},{\"field\":\"LP\",\"label\":\"Lote\"}]}"); + map.put("MY", "{\"name\":\"MALAYSIA\",\"lang\":\"ms\",\"languages\":\"ms\",\"fmt\":\"%N%n%O%n%A%n%D%n%Z %C%n%S\",\"require\":\"ACZ\",\"upper\":\"CS\",\"sublocality_name_type\":\"village_township\",\"state_name_type\":\"state\"}"); + map.put("MZ", "{\"name\":\"MOZAMBIQUE\",\"lang\":\"pt\",\"languages\":\"pt\",\"fmt\":\"%N%n%O%n%A%n%Z %C%S\"}"); + map.put("NA", "{\"name\":\"NAMIBIA\"}"); + map.put("NC", "{\"name\":\"NEW CALEDONIA\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("NE", "{\"name\":\"NIGER\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("NF", "{\"name\":\"NORFOLK ISLAND\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}"); + map.put("NG", "{\"name\":\"NIGERIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D%n%C %Z%n%S\",\"upper\":\"CS\",\"state_name_type\":\"state\",\"label_overrides\":[{\"field\":\"D\",\"label\":\"Local government area\",\"lang\":\"en\"}]}"); + map.put("NI", "{\"name\":\"NICARAGUA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C, %S\",\"upper\":\"CS\",\"state_name_type\":\"department\"}"); + map.put("NL", "{\"name\":\"NETHERLANDS\",\"fmt\":\"%O%n%N%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("NO", "{\"name\":\"NORWAY\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\"}"); + map.put("NP", "{\"name\":\"NEPAL\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("NR", "{\"name\":\"NAURU CENTRAL PACIFIC\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%S\",\"require\":\"AS\",\"state_name_type\":\"district\"}"); + map.put("NU", "{\"name\":\"NIUE\"}"); + map.put("NZ", "{\"name\":\"NEW ZEALAND\",\"fmt\":\"%N%n%O%n%A%n%D%n%C %Z\",\"require\":\"ACZ\"}"); + map.put("OM", "{\"name\":\"OMAN\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C\"}"); + map.put("PA", "{\"name\":\"PANAMA (REP.)\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"CS\"}"); + map.put("PE", "{\"name\":\"PERU\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"locality_name_type\":\"district\"}"); + map.put("PF", "{\"name\":\"FRENCH POLYNESIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"state_name_type\":\"island\"}"); + map.put("PG", "{\"name\":\"PAPUA NEW GUINEA\",\"fmt\":\"%N%n%O%n%A%n%C %Z %S\",\"require\":\"ACS\"}"); + map.put("PH", "{\"name\":\"PHILIPPINES\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D, %C%n%Z %S\"}"); + map.put("PK", "{\"name\":\"PAKISTAN\",\"fmt\":\"%N%n%O%n%A%n%C-%Z\"}"); + map.put("PL", "{\"name\":\"POLAND\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("PM", "{\"name\":\"ST. PIERRE AND MIQUELON\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("PN", "{\"name\":\"PITCAIRN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("PR", "{\"name\":\"PUERTO RICO\",\"fmt\":\"%N%n%O%n%A%n%C PR %Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\",\"zip_name_type\":\"zip\",\"postprefix\":\"PR \"}"); + map.put("PS", "{\"name\":\"PALESTINIAN TERRITORY\"}"); + map.put("PT", "{\"name\":\"PORTUGAL\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}"); + map.put("PW", "{\"name\":\"PALAU\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("PY", "{\"name\":\"PARAGUAY\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("QA", "{\"name\":\"QATAR\",\"upper\":\"AC\"}"); + map.put("RE", "{\"name\":\"REUNION\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("RO", "{\"name\":\"ROMANIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"upper\":\"AC\"}"); + map.put("RS", "{\"name\":\"REPUBLIC OF SERBIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("RU", "{\"name\":\"RUSSIAN FEDERATION\",\"lang\":\"ru\",\"languages\":\"ru\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"require\":\"ACSZ\",\"upper\":\"AC\",\"state_name_type\":\"oblast\",\"label_overrides\":[{\"field\":\"CS\",\"message\":\"MSG_OFFICE_UNIT_NUMBER\"}]}"); + map.put("RW", "{\"name\":\"RWANDA\",\"upper\":\"AC\"}"); + map.put("SA", "{\"name\":\"SAUDI ARABIA\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("SB", "{\"name\":\"SOLOMON ISLANDS\"}"); + map.put("SC", "{\"name\":\"SEYCHELLES\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"S\",\"state_name_type\":\"island\"}"); + map.put("SD", "{\"name\":\"SUDAN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"locality_name_type\":\"district\"}"); + map.put("SE", "{\"name\":\"SWEDEN\",\"fmt\":\"%O%n%N%n%A%nSE-%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\",\"postprefix\":\"SE-\"}"); + map.put("SG", "{\"name\":\"REP. OF SINGAPORE\",\"fmt\":\"%N%n%O%n%A%nSINGAPORE %Z\",\"require\":\"AZ\"}"); + map.put("SH", "{\"name\":\"SAINT HELENA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("SI", "{\"name\":\"SLOVENIA\",\"fmt\":\"%N%n%O%n%A%nSI-%Z %C\",\"postprefix\":\"SI-\"}"); + map.put("SJ", "{\"name\":\"SVALBARD AND JAN MAYEN ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\"}"); + map.put("SK", "{\"name\":\"SLOVAKIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"label_overrides\":[{\"field\":\"NH\",\"label\":\"Obecní část\",\"lang\":\"cs\"},{\"field\":\"NH\",\"label\":\"Obecný časť\",\"lang\":\"sk\"},{\"field\":\"BI\",\"label\":\"Descriptive No.\"},{\"field\":\"BI\",\"label\":\"Popisné číslo\",\"lang\":\"cs\"},{\"field\":\"BI\",\"label\":\"Súpisné číslo\",\"lang\":\"sk\"},{\"field\":\"SN\",\"label\":\"Orientation No.\"},{\"field\":\"SN\",\"label\":\"Orientační číslo\",\"lang\":\"cs\"},{\"field\":\"SN\",\"label\":\"Orientačné číslo\",\"lang\":\"sk\"},{\"field\":\"S1\",\"label\":\"City District\"},{\"field\":\"S1\",\"label\":\"Městská část\",\"lang\":\"cs\"},{\"field\":\"S1\",\"label\":\"Mestská časť\",\"lang\":\"sk\"}]}"); + map.put("SL", "{\"name\":\"SIERRA LEONE\"}"); + map.put("SM", "{\"name\":\"SAN MARINO\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"AZ\"}"); + map.put("SN", "{\"name\":\"SENEGAL\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("SO", "{\"name\":\"SOMALIA\",\"lang\":\"so\",\"languages\":\"so\",\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"require\":\"ACS\",\"upper\":\"ACS\"}"); + map.put("SR", "{\"name\":\"SURINAME\",\"lang\":\"nl\",\"languages\":\"nl\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"AS\"}"); + map.put("SS", "{\"name\":\"SOUTH SUDAN\"}"); + map.put("ST", "{\"name\":\"SAO TOME AND PRINCIPE\"}"); + map.put("SV", "{\"name\":\"EL SALVADOR\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z-%C%n%S\",\"require\":\"ACS\",\"upper\":\"CSZ\"}"); + map.put("SX", "{\"name\":\"SINT MAARTEN\"}"); + map.put("SY", "{\"name\":\"SYRIA\",\"locality_name_type\":\"district\"}"); + map.put("SZ", "{\"name\":\"SWAZILAND\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"upper\":\"ACZ\"}"); + map.put("TA", "{\"name\":\"TRISTAN DA CUNHA\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\"}"); + map.put("TC", "{\"name\":\"TURKS AND CAICOS ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}"); + map.put("TD", "{\"name\":\"CHAD\"}"); + map.put("TF", "{\"name\":\"FRENCH SOUTHERN TERRITORIES\"}"); + map.put("TG", "{\"name\":\"TOGO\"}"); + map.put("TH", "{\"name\":\"THAILAND\",\"lang\":\"th\",\"languages\":\"th\",\"lfmt\":\"%N%n%O%n%A%n%D, %C%n%S %Z\",\"fmt\":\"%N%n%O%n%A%n%D %C%n%S %Z\",\"upper\":\"S\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"Amphoe / Khet\"},{\"field\":\"C\",\"label\":\"อำเภอ/เขต\",\"lang\":\"th\"},{\"field\":\"C\",\"label\":\"アムプー/ケート\",\"lang\":\"ja\"},{\"field\":\"C\",\"label\":\"암프/켓\",\"lang\":\"ko\"},{\"field\":\"C\",\"label\":\"郡/区\",\"lang\":\"zh\"},{\"field\":\"C\",\"label\":\"郡/區\",\"lang\":\"zh-TW\"},{\"field\":\"C\",\"label\":\"郡/區\",\"lang\":\"zh-HK\"},{\"field\":\"D\",\"label\":\"Tambon / Khwaeng\"},{\"field\":\"D\",\"label\":\"ตำบล/แขวง\",\"lang\":\"th\"},{\"field\":\"D\",\"label\":\"タムボン/クウェーン\",\"lang\":\"ja\"},{\"field\":\"D\",\"label\":\"땀본/쾡\",\"lang\":\"ko\"},{\"field\":\"D\",\"label\":\"区/小区\",\"lang\":\"zh\"},{\"field\":\"D\",\"label\":\"區/小區\",\"lang\":\"zh-TW\"},{\"field\":\"D\",\"label\":\"區/小區\",\"lang\":\"zh-HK\"}]}"); + map.put("TJ", "{\"name\":\"TAJIKISTAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("TK", "{\"name\":\"TOKELAU\"}"); + map.put("TL", "{\"name\":\"TIMOR-LESTE\"}"); + map.put("TM", "{\"name\":\"TURKMENISTAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("TN", "{\"name\":\"TUNISIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("TO", "{\"name\":\"TONGA\"}"); + map.put("TR", "{\"name\":\"TURKEY\",\"lang\":\"tr\",\"languages\":\"tr\",\"fmt\":\"%N%n%O%n%A%n%Z %C/%S\",\"require\":\"ACZ\",\"locality_name_type\":\"district\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"İlçe\",\"lang\":\"tr\"},{\"field\":\"S\",\"label\":\"İl\",\"lang\":\"tr\"},{\"field\":\"A4\",\"message\":\"MSG_NEIGHBORHOOD\"}]}"); + map.put("TT", "{\"name\":\"TRINIDAD AND TOBAGO\"}"); + map.put("TV", "{\"name\":\"TUVALU\",\"lang\":\"tyv\",\"languages\":\"tyv\",\"fmt\":\"%N%n%O%n%A%n%C%n%S\",\"upper\":\"ACS\",\"state_name_type\":\"island\"}"); + map.put("TW", "{\"name\":\"TAIWAN\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant\",\"lfmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"fmt\":\"%Z%n%S%C%n%A%n%O%n%N\",\"require\":\"ACSZ\",\"state_name_type\":\"county\"}"); + map.put("TZ", "{\"name\":\"TANZANIA (UNITED REP.)\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("UA", "{\"name\":\"UKRAINE\",\"lang\":\"uk\",\"languages\":\"uk\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"require\":\"ACSZ\",\"state_name_type\":\"oblast\",\"label_overrides\":[{\"field\":\"CS\",\"message\":\"MSG_OFFICE_UNIT_NUMBER\"}]}"); + map.put("UG", "{\"name\":\"UGANDA\"}"); + map.put("UM", "{\"name\":\"UNITED STATES MINOR OUTLYING ISLANDS\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACS\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("US", "{\"name\":\"UNITED STATES\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\",\"width_overrides\":\"%S:S\"}"); + map.put("UY", "{\"name\":\"URUGUAY\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"upper\":\"CS\"}"); + map.put("UZ", "{\"name\":\"UZBEKISTAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\",\"upper\":\"CS\"}"); + map.put("VA", "{\"name\":\"VATICAN\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("VC", "{\"name\":\"SAINT VINCENT AND THE GRENADINES (ANTILLES)\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}"); + map.put("VE", "{\"name\":\"VENEZUELA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%C %Z, %S\",\"require\":\"ACS\",\"upper\":\"CS\",\"state_name_type\":\"state\"}"); + map.put("VG", "{\"name\":\"VIRGIN ISLANDS (BRITISH)\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"A\"}"); + map.put("VI", "{\"name\":\"VIRGIN ISLANDS (U.S.)\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}"); + map.put("VN", "{\"name\":\"VIET NAM\",\"lang\":\"vi\",\"languages\":\"vi\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"label_overrides\":[{\"field\":\"S1\",\"label\":\"Ward/Township/Commune\"},{\"field\":\"S1\",\"label\":\"Phường/Thị trấn/Xã\",\"lang\":\"vi\"}]}"); + map.put("VU", "{\"name\":\"VANUATU\"}"); + map.put("WF", "{\"name\":\"WALLIS AND FUTUNA ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("WS", "{\"name\":\"SAMOA\"}"); + map.put("XK", "{\"name\":\"KOSOVO\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("YE", "{\"name\":\"YEMEN\"}"); + map.put("YT", "{\"name\":\"MAYOTTE\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}"); + map.put("ZA", "{\"name\":\"SOUTH AFRICA\",\"fmt\":\"%N%n%O%n%A%n%D%n%C%n%Z\",\"require\":\"ACZ\"}"); + map.put("ZM", "{\"name\":\"ZAMBIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}"); + map.put("ZW", "{\"name\":\"ZIMBABWE\"}"); + map.put("ZZ", "{\"fmt\":\"%N%n%O%n%A%n%C\",\"require\":\"AC\",\"upper\":\"C\",\"sublocality_name_type\":\"suburb\",\"locality_name_type\":\"city\",\"state_name_type\":\"province\",\"zip_name_type\":\"postal\"}"); + return Collections.unmodifiableMap(map); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/SimpleClientCacheManager.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/SimpleClientCacheManager.java new file mode 100644 index 00000000000..4cbad684e4f --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/SimpleClientCacheManager.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +/** + * A simple implementation of ClientCacheManager which doesn't do any caching on its own. + */ +// This is an external class and part of the widget's public API. +// TODO: Review public API for external classes and tidy JavaDoc. +public final class SimpleClientCacheManager implements ClientCacheManager { + // URL to get public address data. + static final String PUBLIC_ADDRESS_SERVER = "https://chromium-i18n.appspot.com/ssl-address"; + + @Override + public String get(String key) { + return ""; + } + + @Override + public void put(String key, String data) { + } + + @Override + public String getAddressServerUrl() { + return PUBLIC_ADDRESS_SERVER; + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java new file mode 100644 index 00000000000..8eb369301e1 --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import static com.google.i18n.addressinput.common.AddressField.ADMIN_AREA; +import static com.google.i18n.addressinput.common.AddressField.COUNTRY; +import static com.google.i18n.addressinput.common.AddressField.DEPENDENT_LOCALITY; +import static com.google.i18n.addressinput.common.AddressField.LOCALITY; +import static com.google.i18n.addressinput.common.AddressField.ORGANIZATION; +import static com.google.i18n.addressinput.common.AddressField.POSTAL_CODE; +import static com.google.i18n.addressinput.common.AddressField.RECIPIENT; +import static com.google.i18n.addressinput.common.AddressField.SORTING_CODE; +import static com.google.i18n.addressinput.common.AddressField.STREET_ADDRESS; + +import com.google.i18n.addressinput.common.LookupKey.ScriptType; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Performs various consistency checks on an AddressData. This uses a {@link FieldVerifier} to check + * each field in the address. + */ +public final class StandardAddressVerifier { + + private static final String LOCALE_DELIMITER = "--"; + + protected final FieldVerifier rootVerifier; + + protected final Map<AddressField, List<AddressProblemType>> problemMap; + + /** + * Uses the rootVerifier to perform the standard checks on the address fields, as defined in + * {@link StandardChecks}. + */ + public StandardAddressVerifier(FieldVerifier rootVerifier) { + this(rootVerifier, StandardChecks.PROBLEM_MAP); + } + + /** + * Uses the rootVerifier to perform the given checks on the address fields. A reference to + * problemMap is maintained. It is not modified by this class, and should not be modified + * subsequent to this call. + */ + public StandardAddressVerifier(FieldVerifier rootVerifier, + Map<AddressField, List<AddressProblemType>> problemMap) { + this.rootVerifier = rootVerifier; + this.problemMap = problemMap; + } + + /** + * Verifies the address, reporting problems to problems. + */ + public void verify(AddressData address, AddressProblems problems) { + new Verifier(address, problems, new NotifyingListener()).run(); + } + + public void verifyAsync( + AddressData address, AddressProblems problems, DataLoadListener listener) { + Thread verifier = new Thread(new Verifier(address, problems, listener)); + verifier.start(); + } + + /** + * Verifies only the specified fields in the address. + */ + public void verifyFields( + AddressData address, AddressProblems problems, EnumSet<AddressField> addressFieldsToVerify) { + new Verifier(address, problems, new NotifyingListener(), addressFieldsToVerify).run(); + } + + private class Verifier implements Runnable { + private AddressData address; + private AddressProblems problems; + private DataLoadListener listener; + private EnumSet<AddressField> addressFieldsToVerify; + + Verifier(AddressData address, AddressProblems problems, DataLoadListener listener) { + this(address, problems, listener, EnumSet.allOf(AddressField.class)); + } + + Verifier( + AddressData address, AddressProblems problems, DataLoadListener listener, + EnumSet<AddressField> addressFieldsToVerify) { + this.address = address; + this.problems = problems; + this.listener = listener; + this.addressFieldsToVerify = addressFieldsToVerify; + } + + @Override + public void run() { + listener.dataLoadingBegin(); + + FieldVerifier v = rootVerifier; + + ScriptType script = null; + if (address.getLanguageCode() != null) { + if (Util.isExplicitLatinScript(address.getLanguageCode())) { + script = ScriptType.LATIN; + } else { + script = ScriptType.LOCAL; + } + } + + // The first four calls refine the verifier, so must come first, and in this + // order. + verifyFieldIfSelected(script, v, COUNTRY, address.getPostalCountry(), problems); + if (isFieldSelected(COUNTRY) && problems.isEmpty()) { + // Ensure we start with the right language country sub-key. + String countrySubKey = address.getPostalCountry(); + if (address.getLanguageCode() != null && !address.getLanguageCode().equals("")) { + countrySubKey += (LOCALE_DELIMITER + address.getLanguageCode()); + } + v = v.refineVerifier(countrySubKey); + verifyFieldIfSelected(script, v, ADMIN_AREA, address.getAdministrativeArea(), problems); + if (isFieldSelected(ADMIN_AREA) && problems.isEmpty()) { + v = v.refineVerifier(address.getAdministrativeArea()); + verifyFieldIfSelected(script, v, LOCALITY, address.getLocality(), problems); + if (isFieldSelected(LOCALITY) && problems.isEmpty()) { + v = v.refineVerifier(address.getLocality()); + verifyFieldIfSelected( + script, v, DEPENDENT_LOCALITY, address.getDependentLocality(), problems); + if (isFieldSelected(DEPENDENT_LOCALITY) && problems.isEmpty()) { + v = v.refineVerifier(address.getDependentLocality()); + } + } + } + } + + // This concatenation is for the purpose of validation only - the important part is to check + // we have at least one value filled in for lower-level components. + String street = + Util.joinAndSkipNulls("\n", address.getAddressLine1(), + address.getAddressLine2()); + + // Remaining calls don't change the field verifier. + verifyFieldIfSelected(script, v, POSTAL_CODE, address.getPostalCode(), problems); + verifyFieldIfSelected(script, v, STREET_ADDRESS, street, problems); + verifyFieldIfSelected(script, v, SORTING_CODE, address.getSortingCode(), problems); + verifyFieldIfSelected(script, v, ORGANIZATION, address.getOrganization(), problems); + verifyFieldIfSelected(script, v, RECIPIENT, address.getRecipient(), problems); + + postVerify(v, address, problems); + + listener.dataLoadingEnd(); + } + + /** + * Skips address fields that are not included in {@code addressFieldsToVerify}. + */ + private boolean verifyFieldIfSelected(LookupKey.ScriptType script, FieldVerifier verifier, + AddressField field, String value, AddressProblems problems) { + if (!isFieldSelected(field)) { + return true; + } + + return verifyField(script, verifier, field, value, problems); + } + + private boolean isFieldSelected(AddressField field) { + return addressFieldsToVerify.contains(field); + } + } + + /** + * Hook to perform any final processing using the final verifier. Default does no additional + * verification. + */ + protected void postVerify(FieldVerifier verifier, AddressData address, AddressProblems problems) { + } + + /** + * Hook called by verify with each verifiable field, in order. Override to provide pre- or + * post-checks for all fields. + */ + protected boolean verifyField(LookupKey.ScriptType script, FieldVerifier verifier, + AddressField field, String value, AddressProblems problems) { + Iterator<AddressProblemType> iter = getProblemIterator(field); + while (iter.hasNext()) { + AddressProblemType prob = iter.next(); + if (!verifyProblemField(script, verifier, prob, field, value, problems)) { + return false; + } + } + return true; + } + + /** + * Hook for on-the-fly modification of the problem list. Override to change the problems to + * check for a particular field. Generally, changing the problemMap passed to the constructor + * is a better approach. + */ + protected Iterator<AddressProblemType> getProblemIterator(AddressField field) { + List<AddressProblemType> list = problemMap.get(field); + if (list == null) { + list = Collections.emptyList(); + } + return list.iterator(); + } + + /** + * Hook for adding special checks for particular problems and/or fields. + */ + protected boolean verifyProblemField(LookupKey.ScriptType script, FieldVerifier verifier, + AddressProblemType problem, AddressField field, String datum, AddressProblems problems) { + return verifier.check(script, problem, field, datum, problems); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardChecks.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardChecks.java new file mode 100644 index 00000000000..84ad95b3a5e --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/StandardChecks.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import static com.google.i18n.addressinput.common.AddressField.ADMIN_AREA; +import static com.google.i18n.addressinput.common.AddressField.COUNTRY; +import static com.google.i18n.addressinput.common.AddressField.DEPENDENT_LOCALITY; +import static com.google.i18n.addressinput.common.AddressField.LOCALITY; +import static com.google.i18n.addressinput.common.AddressField.ORGANIZATION; +import static com.google.i18n.addressinput.common.AddressField.POSTAL_CODE; +import static com.google.i18n.addressinput.common.AddressField.RECIPIENT; +import static com.google.i18n.addressinput.common.AddressField.SORTING_CODE; +import static com.google.i18n.addressinput.common.AddressField.STREET_ADDRESS; +import static com.google.i18n.addressinput.common.AddressProblemType.INVALID_FORMAT; +import static com.google.i18n.addressinput.common.AddressProblemType.MISMATCHING_VALUE; +import static com.google.i18n.addressinput.common.AddressProblemType.MISSING_REQUIRED_FIELD; +import static com.google.i18n.addressinput.common.AddressProblemType.UNEXPECTED_FIELD; +import static com.google.i18n.addressinput.common.AddressProblemType.UNKNOWN_VALUE; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Loader for a map defining the standard checks to perform on AddressFields. + */ +public final class StandardChecks { + private StandardChecks() { + } + + public static final Map<AddressField, List<AddressProblemType>> PROBLEM_MAP; + + static { + Map<AddressField, List<AddressProblemType>> map = + new HashMap<AddressField, List<AddressProblemType>>(); + + addToMap(map, COUNTRY, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD, UNKNOWN_VALUE); + addToMap(map, ADMIN_AREA, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD, UNKNOWN_VALUE); + addToMap(map, LOCALITY, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD, UNKNOWN_VALUE); + addToMap(map, DEPENDENT_LOCALITY, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD, UNKNOWN_VALUE); + addToMap(map, POSTAL_CODE, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD, INVALID_FORMAT, + MISMATCHING_VALUE); + addToMap(map, STREET_ADDRESS, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD); + addToMap(map, SORTING_CODE, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD); + addToMap(map, ORGANIZATION, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD); + addToMap(map, RECIPIENT, UNEXPECTED_FIELD, MISSING_REQUIRED_FIELD); + + PROBLEM_MAP = Collections.unmodifiableMap(map); + } + + private static void addToMap(Map<AddressField, List<AddressProblemType>> map, AddressField field, + AddressProblemType... problems) { + map.put(field, Collections.unmodifiableList(Arrays.asList(problems))); + } +} diff --git a/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/Util.java b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/Util.java new file mode 100644 index 00000000000..c63d93e981d --- /dev/null +++ b/chromium/third_party/libaddressinput/src/common/src/main/java/com/google/i18n/addressinput/common/Util.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.google.i18n.addressinput.common; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility functions used by the address widget. + */ +public final class Util { + /** + * This variable is in upper-case, since we convert the language code to upper case before doing + * string comparison. + */ + private static final String LATIN_SCRIPT = "LATN"; + + /** + * Map of countries that have non-latin local names, with the language that their local names + * are in. We only list a country here if we have the appropriate data. Only language sub-tags + * are listed. + * TODO(user): Delete this: the information should be read from RegionDataConstants.java. + */ + private static final Map<String, String> nonLatinLocalLanguageCountries = + new HashMap<String, String>(); + static { + nonLatinLocalLanguageCountries.put("AE", "ar"); + nonLatinLocalLanguageCountries.put("AM", "hy"); + nonLatinLocalLanguageCountries.put("CN", "zh"); + nonLatinLocalLanguageCountries.put("EG", "ar"); + nonLatinLocalLanguageCountries.put("HK", "zh"); + nonLatinLocalLanguageCountries.put("JP", "ja"); + nonLatinLocalLanguageCountries.put("KP", "ko"); + nonLatinLocalLanguageCountries.put("KR", "ko"); + nonLatinLocalLanguageCountries.put("MO", "zh"); + nonLatinLocalLanguageCountries.put("RU", "ru"); + nonLatinLocalLanguageCountries.put("TH", "th"); + nonLatinLocalLanguageCountries.put("TW", "zh"); + nonLatinLocalLanguageCountries.put("UA", "uk"); + nonLatinLocalLanguageCountries.put("VN", "vi"); + } + + /** + * Cannot instantiate this class - private constructor. + */ + private Util() { + } + + /** + * Returns true if the language code is explicitly marked to be in the latin script. For + * example, "zh-Latn" would return true, but "zh-TW", "en" and "zh" would all return false. + */ + public static boolean isExplicitLatinScript(String languageCode) { + // Convert to upper-case for easier comparison. + languageCode = toUpperCaseLocaleIndependent(languageCode); + // Check to see if the language code contains a script modifier. + final Pattern languageCodePattern = Pattern.compile("\\w{2,3}[-_](\\w{4})"); + Matcher m = languageCodePattern.matcher(languageCode); + if (m.lookingAt()) { + String script = m.group(1); + if (script.equals(LATIN_SCRIPT)) { + return true; + } + } + return false; + } + + /** + * Returns the language subtag of a language code. For example, returns "zh" if given "zh-Hans", + * "zh-CN" or other "zh" variants. If no language subtag can be found or the language tag is + * malformed, returns "und". + */ + public static String getLanguageSubtag(String languageCode) { + final Pattern languageCodePattern = Pattern.compile("(\\w{2,3})(?:[-_]\\w{4})?(?:[-_]\\w{2})?"); + Matcher m = languageCodePattern.matcher(languageCode); + if (m.matches()) { + return toLowerCaseLocaleIndependent(m.group(1)); + } + return "und"; + } + + /** + * Trims the string. If the field is empty after trimming, returns null instead. Note that this + * only trims ASCII white-space. + */ + static String trimToNull(String originalStr) { + if (originalStr == null) { + return null; + } + String trimmedString = originalStr.trim(); + return (trimmedString.length() == 0) ? null : trimmedString; + } + + /** + * Throws an exception if the object is null, with a generic error message. + */ + static <T> T checkNotNull(T o) { + return checkNotNull(o, "This object should not be null."); + } + + /** + * Throws an exception if the object is null, with the error message supplied. + */ + static <T> T checkNotNull(T o, String message) { + if (o == null) { + throw new NullPointerException(message); + } + return o; + } + + /** + * Joins input string with the given separator. If an input string is null, it will be skipped. + */ + static String joinAndSkipNulls(String separator, String... strings) { + StringBuilder sb = null; + for (String s : strings) { + if (s != null) { + s = s.trim(); + if (s.length() > 0) { + if (sb == null) { + sb = new StringBuilder(s); + } else { + sb.append(separator).append(s); + } + } + } + } + return sb == null ? null : sb.toString(); + } + + /** + * Builds a map of the lower-cased values of the keys, names and local names provided. Each name + * and local name is mapped to its respective key in the map. + * + * @throws IllegalStateException if the names or lnames array is greater than the keys array. + */ + static Map<String, String> buildNameToKeyMap(String[] keys, String[] names, String[] lnames) { + if (keys == null) { + return null; + } + + Map<String, String> nameToKeyMap = new HashMap<String, String>(); + + int keyLength = keys.length; + for (String k : keys) { + nameToKeyMap.put(toLowerCaseLocaleIndependent(k), k); + } + if (names != null) { + if (names.length > keyLength) { + throw new IllegalStateException("names length (" + names.length + + ") is greater than keys length (" + keys.length + ")"); + } + for (int i = 0; i < keyLength; i++) { + // If we have less names than keys, we ignore all missing names. This happens + // generally because reg-ex splitting methods on different platforms (java, js etc) + // behave differently in the default case. Since missing names are fine, we opt to + // be more robust here. + if (i < names.length && names[i].length() > 0) { + nameToKeyMap.put(toLowerCaseLocaleIndependent(names[i]), keys[i]); + } + } + } + if (lnames != null) { + if (lnames.length > keyLength) { + throw new IllegalStateException("lnames length (" + lnames.length + + ") is greater than keys length (" + keys.length + ")"); + } + for (int i = 0; i < keyLength; i++) { + if (i < lnames.length && lnames[i].length() > 0) { + nameToKeyMap.put(toLowerCaseLocaleIndependent(lnames[i]), keys[i]); + } + } + } + return nameToKeyMap; + } + + /** + * Returns a language code that the widget can use when fetching data, based on a {@link + * java.util.Locale} language and the current selected country in the address widget. This + * method is necessary since we have to determine later whether a language is "local" or "latin" + * for certain countries. + * + * @param language the current user language + * @param currentCountry the current selected country + * @return a language code string in BCP-47 format (e.g. "en", "zh-Latn", "zh-Hans" or + * "en-US"). + */ + public static String getWidgetCompatibleLanguageCode(Locale language, String currentCountry) { + String country = toUpperCaseLocaleIndependent(currentCountry); + // Only do something if the country is one of those where we have names in the local + // language as well as in latin script. + if (nonLatinLocalLanguageCountries.containsKey(country)) { + String languageTag = language.getLanguage(); + // Only do something if the language tag is _not_ the local language. + if (!languageTag.equals(nonLatinLocalLanguageCountries.get(country))) { + // Build up the language tag with the country and language specified, and add in the + // script-tag of "Latn" explicitly, since this is _not_ a local language. This means + // that we might create a language tag of "th-Latn", which is not what the actual + // language being used is, but it indicates that we prefer "Latn" names to whatever + // the local alternative was. + StringBuilder languageTagBuilder = new StringBuilder(languageTag); + languageTagBuilder.append("_latn"); + if (language.getCountry().length() > 0) { + languageTagBuilder.append("_"); + languageTagBuilder.append(language.getCountry()); + } + return languageTagBuilder.toString(); + } + } + return language.toString(); + } + + /** + * Converts all of the characters in this String to lower case using the rules of English. This is + * equivalent to calling toLowerCase(Locale.ENGLISH). Thus avoiding locale-sensitive case folding + * such as the Turkish i, which could mess e.g. with lookup keys and country codes. + */ + public static String toLowerCaseLocaleIndependent(String value) { + return (value != null) ? value.toLowerCase(Locale.ENGLISH) : null; + } + + /** + * Converts all of the characters in this String to upper case using the rules of English. This is + * equivalent to calling toUpperCase(Locale.ENGLISH). Thus avoiding locale-sensitive case folding + * such as the Turkish i, which could mess e.g. with lookup keys and country codes. + */ + public static String toUpperCaseLocaleIndependent(String value) { + return (value != null) ? value.toUpperCase(Locale.ENGLISH) : null; + } +} |