(AUTOMATIC) opensource update (#125)
diff --git a/README.md b/README.md
index 4c932a1..535172d 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,16 @@
The C++ library (in very portable C++03) of _libaddressinput_ is used in address-related
projects in [Chromium](http://www.chromium.org/Home).
+[requestAutocomplete()](http://www.html5rocks.com/en/tutorials/forms/requestautocomplete/)
+in [Chromium](http://www.chromium.org/Home). The source code for that is a good
+example of how to use this library to implement a complex feature in a real
+application:
+https://src.chromium.org/viewvc/chrome/trunk/src/third_party/libaddressinput/
https://chromium.googlesource.com/chromium/src/+/master/third_party/libaddressinput/
+Video: [Easy International Checkout with Chrome](https://www.youtube.com/watch?v=ljYeHwGgzQk)
+
## Java
The Java library of _libaddressinput_ is written for use in
diff --git a/android/README b/android/README
deleted file mode 100644
index b044bd0..0000000
--- a/android/README
+++ /dev/null
@@ -1,45 +0,0 @@
-Building and running tests with Android
-=======================================
-
-The easiest way to build libaddressinput for Android and run all the tests is
-using the Gradle project automation tool:
-
-http://tools.android.com/tech-docs/new-build-system
-http://www.gradle.org/
-
-
-Prerequisite dependencies for using Gradle with Android
--------------------------------------------------------
-Android Studio: https://developer.android.com/sdk/index.html
-or
-Android SDK Tools: https://developer.android.com/sdk/index.html#Other
-
-Set the ANDROID_HOME environment variable to the root of the SDK.
-
-Install the following packages:
-* Tools/Android SDK Build-tools (Rev. 21.1.2)
-* Android 5.1 (API 22)
-* Extras/Android Support Library
-
-Gradle (latest version):
- https://services.gradle.org/distributions/gradle-2.3-bin.zip
-
-Note: Additionally you must take care to avoid having multiple versions of
-Gradle on your path, as this can cause problems.
-
-
-Building and Running
---------------------
-After installing all the prerequisites, check that everything is working by
-running:
-
-$ gradle build
-
-With an Android emulator running or an Android device connected, the following
-command line then builds the library and runs the tests:
-
-$ gradle connectedAndroidTest
-
-The test runner logs to the system log, which can be viewed using logcat:
-
-$ adb logcat
diff --git a/android/README.md b/android/README.md
new file mode 100644
index 0000000..03af760
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,134 @@
+# Building and running tests with Android
+
+
+The easiest way to build libaddressinput for Android and run all the tests is
+using the Gradle project automation tool:
+
+http://tools.android.com/tech-docs/new-build-system
+http://www.gradle.org/
+
+
+## Prerequisite dependencies for using Gradle with Android
+
+Android Studio: https://developer.android.com/sdk/index.html
+or
+Android SDK Tools: https://developer.android.com/sdk/index.html#Other
+
+Set the ANDROID_HOME environment variable to the root of the SDK.
+
+Install the following packages:
+* Tools/Android SDK Build-tools (Rev. 21.1.2)
+* Android 5.1 (API 22)
+* Extras/Android Support Library
+
+Gradle (latest version):
+ https://services.gradle.org/distributions/gradle-2.3-bin.zip
+
+Note: Additionally you must take care to avoid having multiple versions of
+Gradle on your path, as this can cause problems.
+
+
+## Building and Running
+
+After installing all the prerequisites, check that everything is working by
+running:
+
+$ gradle build
+
+With an Android emulator running or an Android device connected, the following
+command line then builds the library and runs the tests:
+
+$ gradle connectedAndroidTest
+
+The test runner logs to the system log, which can be viewed using logcat:
+
+$ adb logcat
+
+# Integrating with Android Apps
+
+
+1. Clone libaddressinput from Github or download and unzip to a folder called 'libaddressinput'.
+
+
+2. From a terminal window, change into the folder: `cd libaddressinput/`
+
+3. Build the widget and library via gradle:
+
+ `gradle build`
+
+4. Copy the widget and the common libraries:
+
+ `cp android/build/outputs/aar/android-release.aar path/to/project/app/libs/`
+
+ `cp common/build/libs/common.jar path/to/project/app/libs/`
+
+ Note: Be sure top replace 'path/to/project' with the name of your project.
+
+5. Import both modules into your app.
+
+ Note: If you use Android Studio, check out the [user guide](https://developer.android.com/studio/projects/android-library.html#AddDependency) and follow the instructions under 'Add your library as a dependency'. Be sure to add *both* modules as dependencies of the app.
+
+6. Add the widget to your app. Note: This Assumes a default empty project configuration:
+
+ i. In activity_main.xml add:
+
+ ```
+ <LinearLayout
+ android:id="@+id/addresswidget"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"/>
+ ```
+
+ ii. In MainActivity.java add the following import statements:
+
+ ```
+ import android.view.ViewGroup;
+
+ import com.android.i18n.addressinput.AddressWidget;
+ import com.google.i18n.addressinput.common.FormOptions;
+ import com.google.i18n.addressinput.common.ClientCacheManager;
+ import com.google.i18n.addressinput.common.SimpleClientCacheManager;
+ ```
+
+ iii. Define the widget on the object scope
+
+ `private AddressWidget addressWidget;`
+
+ iv. Add the widget to the ViewGroup
+ ```
+ ViewGroup viewGroup = (ViewGroup) findViewById(R.id.addresswidget);
+ FormOptions defaultFormOptions = new FormOptions();
+ ClientCacheManager cacheManager = new SimpleClientCacheManager();
+ this.addressWidget = new AddressWidget(this, viewGroup, defaultFormOptions, cacheManager);
+ ```
+
+Example:
+
+```
+package com.example.google.widgetdemo;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.ViewGroup;
+
+import com.android.i18n.addressinput.AddressWidget;
+import com.google.i18n.addressinput.common.FormOptions;
+import com.google.i18n.addressinput.common.ClientCacheManager;
+import com.google.i18n.addressinput.common.SimpleClientCacheManager;
+
+public class MainActivity extends AppCompatActivity {
+ private AddressWidget addressWidget;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ ViewGroup viewGroup = (ViewGroup) findViewById(R.id.addresswidget);
+ FormOptions defaultFormOptions = new FormOptions();
+ ClientCacheManager cacheManager = new SimpleClientCacheManager();
+ this.addressWidget = new AddressWidget(this, viewGroup, defaultFormOptions, cacheManager);
+ }
+}
+```
diff --git a/android/build.gradle b/android/build.gradle
index 0d48fc8..a25bd9d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -32,6 +32,8 @@
dependencies {
compile project(':common')
+ compile 'com.google.android.gms:play-services-location:10.0.0'
+ compile 'com.google.android.gms:play-services-places:9.2.0'
}
android {
@@ -56,5 +58,9 @@
*/
compileSdkVersion 22
buildToolsVersion '21.1.2'
+ defaultConfig {
+ minSdkVersion 17
+ targetSdkVersion 22
+ }
}
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java
new file mode 100644
index 0000000..d7898f1
--- /dev/null
+++ b/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/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
similarity index 96%
rename from android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java
rename to android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
index d9f0796..7b558b1 100644
--- a/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java
+++ b/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
@@ -16,24 +16,22 @@
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 com.android.i18n.addressinput.testing.AsyncTestCase;
-
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
-public class AndroidAsyncRequestApiTest extends AsyncTestCase {
+public class AndroidAsyncEncodedRequestApiTest extends AsyncTestCase {
private AsyncRequestApi requestApi;
@Override
public void setUp() {
- requestApi = new AndroidAsyncRequestApi();
+ requestApi = new AndroidAsyncEncodedRequestApi();
}
public void testRequestObject() throws Exception {
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java
new file mode 100644
index 0000000..23f40ee
--- /dev/null
+++ b/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/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java
new file mode 100644
index 0000000..f0495bf
--- /dev/null
+++ b/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/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java b/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java
new file mode 100644
index 0000000..c71eb4e
--- /dev/null
+++ b/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/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java b/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
index a674fec..c1d11a4 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
@@ -20,6 +20,7 @@
import com.google.i18n.addressinput.common.RegionData;
import android.view.View;
+import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.Spinner;
@@ -109,6 +110,37 @@
}
}
+ /**
+ * 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;
}
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java b/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
index 02ee02b..e534327 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
@@ -16,6 +16,23 @@
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;
@@ -34,27 +51,10 @@
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 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.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 java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
@@ -95,6 +95,10 @@
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;
@@ -254,10 +258,33 @@
rootView.addView(textView, lp);
}
if (field.getUiType().equals(UiComponent.EDIT)) {
- EditText editText = componentProvider.createUiTextField(widthType);
- field.setView(editText);
- editText.setEnabled(!readOnly);
- rootView.addView(editText, lp);
+ 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);
@@ -278,6 +305,20 @@
}
}
+ 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.
*/
@@ -428,6 +469,7 @@
private void updateFields() {
removePreviousViews();
+ createViewForCountry();
buildFieldWidgets();
initializeDropDowns();
layoutAddressFields();
@@ -437,15 +479,7 @@
if (rootView == null) {
return;
}
- int childCount = rootView.getChildCount();
- if (formOptions.isHidden(AddressField.COUNTRY)) {
- if (childCount > 0) {
- rootView.removeAllViews();
- }
- } else if (childCount > 2) {
- // Keep the TextView and Spinner for Country and remove everything else.
- rootView.removeViews(2, rootView.getChildCount() - 2);
- }
+ rootView.removeAllViews();
}
private void layoutAddressFields() {
@@ -510,6 +544,7 @@
}
public void renderForm() {
+ createViewForCountry();
setWidgetLocaleAndScript();
AddressData data = new AddressData.Builder().setCountry(currentRegion)
.setLanguageCode(widgetLocale).build();
@@ -620,10 +655,46 @@
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);
}
@@ -634,10 +705,10 @@
if (value == null) {
value = "";
}
+
AddressUiComponent uiComponent = inputWidgets.get(field);
- EditText view = (EditText) uiComponent.getView();
- if (view != null) {
- view.setText(value);
+ if (uiComponent != null) {
+ uiComponent.setValue(value);
}
}
}
@@ -648,17 +719,11 @@
this.rootView = rootView;
this.formOptions = formOptions;
// Inject Adnroid specific async request implementation here.
- this.cacheData = new CacheData(cacheManager, new AndroidAsyncRequestApi());
+ 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));
- if (!formOptions.isHidden(AddressField.COUNTRY)) {
- buildCountryListBox();
- createView(rootView, inputWidgets.get(AddressField.COUNTRY),
- getLocalCountryName(currentRegion),
- formOptions.isReadonly(AddressField.COUNTRY));
- }
}
/**
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java b/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
index def5d72..6931847 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
@@ -22,6 +22,7 @@
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;
@@ -76,6 +77,17 @@
}
/**
+ * 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.
*
diff --git a/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java
new file mode 100644
index 0000000..bc7305c
--- /dev/null
+++ b/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/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
index c507ca5..5fe40e9 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
@@ -20,11 +20,9 @@
import com.google.i18n.addressinput.common.JsoMap;
import java.io.BufferedReader;
import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
-import java.net.URLEncoder;
import java.security.Provider;
import java.security.Security;
import javax.net.ssl.HttpsURLConnection;
@@ -41,14 +39,16 @@
*/
// 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 String requestUrlString;
+ private final URL requestUrl;
private final AsyncCallback callback;
private final int timeoutMillis;
- protected AsyncHttp(String requestUrlString, AsyncCallback callback, int timeoutMillis) {
- this.requestUrlString = requestUrlString;
+ protected AsyncHttp(URL requestUrl, AsyncCallback callback, int timeoutMillis) {
+ this.requestUrl = requestUrl;
this.callback = callback;
this.timeoutMillis = timeoutMillis;
}
@@ -60,8 +60,7 @@
// 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.
- URL url = encodeUrl(requestUrlString);
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection();
connection.setConnectTimeout(timeoutMillis);
connection.setReadTimeout(timeoutMillis);
@@ -99,43 +98,14 @@
}
@Override public void requestObject(String url, AsyncCallback callback, int timeoutMillis) {
- (new AsyncHttp(url, callback, timeoutMillis)).start();
+ try {
+ (new AsyncHttp(stringToUrl(url), callback, timeoutMillis)).start();
+ } catch (MalformedURLException e) {
+ callback.onFailure();
+ }
}
- /**
- * A quick hack to transform a string into an RFC 3986 compliant URL.
- *
- * TODO: Refactor the code to stop passing URLs around as strings, to eliminate the need for
- * this broken hack.
- */
- private static URL encodeUrl(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());
+ protected URL stringToUrl(String url) throws MalformedURLException {
+ return new URL(url);
}
}
diff --git a/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java
new file mode 100644
index 0000000..3bbb617
--- /dev/null
+++ b/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/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java
new file mode 100644
index 0000000..e778911
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java
@@ -0,0 +1,151 @@
+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.
+ */
+class PlaceDetailsClient implements PlaceDetailsApi {
+
+ private AsyncRequestApi asyncRequestApi;
+ private String apiKey;
+
+ @VisibleForTesting static final int TIMEOUT = 5000;
+
+ private static final String TAG = "PlaceDetailsClient";
+
+ 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 {
+ Object result = response.getObject("result");
+ if (result instanceof JSONObject) {
+ addressData.set(getAddressData((JSONObject) result));
+ } else {
+ Log.e(
+ TAG,
+ "Error parsing JSON response from Place Details API: "
+ + "expected 'result' field.");
+ onFailure();
+ }
+ } 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/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java
new file mode 100644
index 0000000..1b8d9a0
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java
@@ -0,0 +1,105 @@
+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}.
+ *
+ * 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/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java
new file mode 100644
index 0000000..d38b450
--- /dev/null
+++ b/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/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml b/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml
new file mode 100644
index 0000000..cf77743
--- /dev/null
+++ b/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+
+The <ripple> element is only available after API level 21, so this polyfill causes the background
+of autocomplete dropdown items to darken when they are clicked.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape>
+ <solid android:drawable="@color/ripple_material_light"/>
+ </shape>
+ </item>
+
+ <item>
+ <shape>
+ <solid android:drawable="@android:color/white"/>
+ </shape>
+ </item>
+</selector>
diff --git a/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml b/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml
new file mode 100644
index 0000000..60b404e
--- /dev/null
+++ b/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/ripple_material_light">
+
+ <item android:drawable="@android:color/white" />
+</ripple>
diff --git a/android/src/main/res/layout/address_autocomplete_dropdown_item.xml b/android/src/main/res/layout/address_autocomplete_dropdown_item.xml
new file mode 100644
index 0000000..57841a8
--- /dev/null
+++ b/android/src/main/res/layout/address_autocomplete_dropdown_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/address_autocomplete_dropdown_item_padding_vertical"
+ android:paddingBottom="@dimen/address_autocomplete_dropdown_item_padding_vertical"
+ android:background="@drawable/autocomplete_dropdown_item_background_selected">
+
+ <TextView
+ style="?android:attr/dropDownItemStyle"
+ android:id="@+id/line_1"
+ android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="left"
+ android:background="@android:color/transparent"/>
+
+ <TextView
+ style="?android:attr/dropDownItemStyle"
+ android:id="@+id/line_2"
+ android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="left"
+ android:background="@android:color/transparent"/>
+
+</LinearLayout>
diff --git a/android/src/main/res/layout/address_autocomplete_textview.xml b/android/src/main/res/layout/address_autocomplete_textview.xml
new file mode 100644
index 0000000..436626b
--- /dev/null
+++ b/android/src/main/res/layout/address_autocomplete_textview.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<AutoCompleteTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/address_autocomplete_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/address_textview_margin_top"
+ android:layout_marginLeft="@dimen/address_textview_margin_left"
+ android:textColor="?android:attr/textColorPrimary"
+ android:focusableInTouchMode="true" />
diff --git a/android/src/main/res/layout/address_textview.xml b/android/src/main/res/layout/address_textview.xml
index f112a5a..1fe05e5 100644
--- a/android/src/main/res/layout/address_textview.xml
+++ b/android/src/main/res/layout/address_textview.xml
@@ -20,7 +20,7 @@
android:id="@+id/address_text_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="4dip"
- android:layout_marginLeft="3dip"
+ android:layout_marginTop="@dimen/address_textview_margin_top"
+ android:layout_marginLeft="@dimen/address_textview_margin_left"
android:textColor="?android:attr/textColorPrimary"
android:focusableInTouchMode="true" />
diff --git a/android/src/main/res/values/address_dimens.xml b/android/src/main/res/values/address_dimens.xml
new file mode 100644
index 0000000..6ba89b6
--- /dev/null
+++ b/android/src/main/res/values/address_dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources>
+ <dimen name="address_textview_margin_top">4dp</dimen>
+ <dimen name="address_textview_margin_left">3dp</dimen>
+
+ <dimen name="address_autocomplete_dropdown_item_padding_vertical">5dp</dimen>
+</resources>
diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d25b5cd
--- /dev/null
+++ b/android/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <item name="ripple_material_light" type="color">#1f000000</item>
+
+ <integer-array name="androidcolors">
+ <item>@color/ripple_material_light</item>
+ </integer-array>
+</resources>
diff --git a/common/README b/common/README.md
similarity index 73%
rename from common/README
rename to common/README.md
index fdec1bd..1e58ecd 100644
--- a/common/README
+++ b/common/README.md
@@ -1,5 +1,5 @@
-Building and running tests
-==========================
+# Building and running tests
+
The common (non-UI) parts of libaddressinput are built and run using the Gradle
project automation tool:
@@ -8,8 +8,8 @@
http://www.gradle.org/
-Prerequisite dependencies for using Gradle
-------------------------------------------
+## Prerequisite dependencies for using Gradle
+
Gradle (latest version):
https://services.gradle.org/distributions/gradle-2.3-bin.zip
@@ -17,8 +17,8 @@
Gradle on your path, as this can cause problems.
-Building and Running
---------------------
+## Building and Running
+
After installing all the prerequisites, check that everything is working by
running:
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java
new file mode 100644
index 0000000..1dd4711
--- /dev/null
+++ b/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/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java
new file mode 100644
index 0000000..6f788a9
--- /dev/null
+++ b/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/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
index 3e2c10e..e30a89a 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
@@ -136,6 +136,9 @@
// 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);
@@ -205,6 +208,76 @@
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
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
index fe31caa..906d56a 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
@@ -18,6 +18,7 @@
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.
@@ -65,4 +66,13 @@
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/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java b/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
index a281797..a8d9763 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
@@ -18,7 +18,6 @@
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;
@@ -47,6 +46,7 @@
// 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;
@@ -71,10 +71,19 @@
private Pattern match;
/**
- * Creates the root field verifier for a particular data source.
+ * 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();
}
@@ -85,9 +94,10 @@
*/
FieldVerifier(FieldVerifier parent, AddressVerificationNodeData nodeData) {
// Most information is inherited from the parent.
- possiblyUsedFields = parent.possiblyUsedFields;
- required = parent.required;
+ 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
@@ -158,8 +168,6 @@
&& keys.length == latinNames.length) {
localNames = keys;
}
- // These fields are populated from RegionDataConstants so that the metadata server can be
- // updated without needing to be in sync with clients.
if (isCountryKey()) {
populatePossibleAndRequired(getRegionCodeFromKey(id));
}
@@ -183,11 +191,7 @@
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.
- LookupKey lookupKey =
- new LookupKey.Builder(Util.toLowerCaseLocaleIndependent(KeyType.DATA.name())
- + KEY_NODE_DELIMITER
- + regionCode).build();
- AddressVerificationNodeData countryNode = dataSource.getDefaultData(lookupKey.toString());
+ AddressVerificationNodeData countryNode = getCountryNode(regionCode);
String languages = countryNode.get(AddressDataKey.LANGUAGES);
String defaultLanguage = countryNode.get(AddressDataKey.LANG);
Set<String> alternateLanguages = new HashSet<String>();
@@ -204,7 +208,40 @@
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);
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
index 1ace84f..ba4c668 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
@@ -61,10 +61,15 @@
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(scriptType, regionCode)) {
+ for (String substring : getFormatSubstrings(formatString)) {
// Skips un-escaped characters and new lines.
if (!substring.matches("%.") || substring.equals(NEW_LINE)) {
continue;
@@ -153,7 +158,10 @@
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));
@@ -241,7 +249,8 @@
}
List<String> prunedFormat = new ArrayList<String>();
- List<String> formatSubstrings = getFormatSubstrings(scriptType, regionCode);
+ 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.
@@ -335,8 +344,7 @@
*/
// 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(ScriptType scriptType, String regionCode) {
- String formatString = getFormatString(scriptType, regionCode);
+ private List<String> getFormatSubstrings(String formatString) {
List<String> parts = new ArrayList<String>();
boolean escaped = false;
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java b/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
index 4f039c2..cc5a2cf 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
@@ -102,7 +102,7 @@
* @return The object associated with the key.
* @throws JSONException if the key is not found.
*/
- private Object getObject(String name) throws JSONException {
+ public Object getObject(String name) throws JSONException {
return super.get(name);
}
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java b/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java
new file mode 100644
index 0000000..b5b9163
--- /dev/null
+++ b/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/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
index 975ca76..c977695 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
@@ -94,7 +94,7 @@
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\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}");
+ 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\"}");
@@ -118,7 +118,7 @@
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 %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
+ 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\"}]}");
@@ -131,7 +131,7 @@
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\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"require\":\"ACSZ\",\"state_name_type\":\"state\",\"zip_name_type\":\"pin\"}");
+ 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\"}");
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\",\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\",\"sublocality_name_type\":\"neighborhood\"}");
@@ -189,7 +189,7 @@
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%C %Z%n%S\",\"upper\":\"CS\",\"state_name_type\":\"state\"}");
+ 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\"}");
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\"}");
@@ -265,7 +265,7 @@
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\"}");
+ 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\"}");
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java b/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
index e1fe22b..b9b3fda 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
@@ -29,6 +29,7 @@
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;
@@ -83,15 +84,31 @@
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
@@ -111,22 +128,23 @@
// The first four calls refine the verifier, so must come first, and in this
// order.
- verifyField(script, v, COUNTRY, address.getPostalCountry(), problems);
- if (problems.isEmpty()) {
+ 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);
- verifyField(script, v, ADMIN_AREA, address.getAdministrativeArea(), problems);
- if (problems.isEmpty()) {
+ verifyFieldIfSelected(script, v, ADMIN_AREA, address.getAdministrativeArea(), problems);
+ if (isFieldSelected(ADMIN_AREA) && problems.isEmpty()) {
v = v.refineVerifier(address.getAdministrativeArea());
- verifyField(script, v, LOCALITY, address.getLocality(), problems);
- if (problems.isEmpty()) {
+ verifyFieldIfSelected(script, v, LOCALITY, address.getLocality(), problems);
+ if (isFieldSelected(LOCALITY) && problems.isEmpty()) {
v = v.refineVerifier(address.getLocality());
- verifyField(script, v, DEPENDENT_LOCALITY, address.getDependentLocality(), problems);
- if (problems.isEmpty()) {
+ verifyFieldIfSelected(
+ script, v, DEPENDENT_LOCALITY, address.getDependentLocality(), problems);
+ if (isFieldSelected(DEPENDENT_LOCALITY) && problems.isEmpty()) {
v = v.refineVerifier(address.getDependentLocality());
}
}
@@ -140,16 +158,32 @@
address.getAddressLine2());
// Remaining calls don't change the field verifier.
- verifyField(script, v, POSTAL_CODE, address.getPostalCode(), problems);
- verifyField(script, v, STREET_ADDRESS, street, problems);
- verifyField(script, v, SORTING_CODE, address.getSortingCode(), problems);
- verifyField(script, v, ORGANIZATION, address.getOrganization(), problems);
- verifyField(script, v, RECIPIENT, address.getRecipient(), problems);
+ 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);
+ }
}
/**
diff --git a/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java b/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
index 2e27ba9..f609c93 100644
--- a/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
+++ b/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
@@ -150,4 +150,171 @@
address = AddressData.builder(address).setLanguageCode("zh-latn").build();
assertEquals("zh-latn", address.getLanguageCode());
}
+
+ @Test
+ public void testEqualsIsSymmetric() {
+ AddressData addressData1 = AddressData.builder().build();
+ AddressData addressData2 = AddressData.builder().build();
+
+ assertThat(addressData1).isEqualTo(addressData2);
+ assertThat(addressData2).isEqualTo(addressData1);
+ }
+
+ @Test
+ public void testEqualsIsSymmetricNotEquals() {
+ AddressData addressData1 = AddressData.builder().setCountry("US").build();
+ AddressData addressData2 = AddressData.builder().build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData2).isNotEqualTo(addressData1);
+ }
+
+ @Test
+ public void testEqualsIsTransitive() {
+ AddressData addressData1 = AddressData.builder().build();
+ AddressData addressData2 = AddressData.builder().build();
+ AddressData addressData3 = AddressData.builder().build();
+
+ assertThat(addressData1).isEqualTo(addressData2);
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData1).isEqualTo(addressData3);
+ }
+
+ @Test
+ public void testEqualsIsNullSafe() {
+ AddressData addressData1 = AddressData.builder().build();
+ AddressData addressData2 = null;
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData2).isNotEqualTo(addressData1);
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareCountry() {
+ AddressData addressData1 = AddressData.builder().setCountry("X").build();
+ AddressData addressData2 = AddressData.builder().setCountry("Y").build();
+ AddressData addressData3 = AddressData.builder().setCountry("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareAddressLines() {
+ AddressData addressData1 = AddressData.builder().setAddress("X").build();
+ AddressData addressData2 = AddressData.builder().setAddress("Y").build();
+ AddressData addressData3 = AddressData.builder().setAddress("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareAdminArea() {
+ AddressData addressData1 = AddressData.builder().setAdminArea("X").build();
+ AddressData addressData2 = AddressData.builder().setAdminArea("Y").build();
+ AddressData addressData3 = AddressData.builder().setAdminArea("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareLocality() {
+ AddressData addressData1 = AddressData.builder().setLocality("X").build();
+ AddressData addressData2 = AddressData.builder().setLocality("Y").build();
+ AddressData addressData3 = AddressData.builder().setLocality("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareDependentLocality() {
+ AddressData addressData1 = AddressData.builder().setDependentLocality("X").build();
+ AddressData addressData2 = AddressData.builder().setDependentLocality("Y").build();
+ AddressData addressData3 = AddressData.builder().setDependentLocality("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeComparePostalCode() {
+ AddressData addressData1 = AddressData.builder().setPostalCode("X").build();
+ AddressData addressData2 = AddressData.builder().setPostalCode("Y").build();
+ AddressData addressData3 = AddressData.builder().setPostalCode("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareSortingCode() {
+ AddressData addressData1 = AddressData.builder().setSortingCode("X").build();
+ AddressData addressData2 = AddressData.builder().setSortingCode("Y").build();
+ AddressData addressData3 = AddressData.builder().setSortingCode("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareOrganization() {
+ AddressData addressData1 = AddressData.builder().setOrganization("X").build();
+ AddressData addressData2 = AddressData.builder().setOrganization("Y").build();
+ AddressData addressData3 = AddressData.builder().setOrganization("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareRecipient() {
+ AddressData addressData1 = AddressData.builder().setRecipient("X").build();
+ AddressData addressData2 = AddressData.builder().setRecipient("Y").build();
+ AddressData addressData3 = AddressData.builder().setRecipient("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
+
+ @Test
+ public void testEqualsAndHashCodeCompareLanguageCode() {
+ AddressData addressData1 = AddressData.builder().setLanguageCode("X").build();
+ AddressData addressData2 = AddressData.builder().setLanguageCode("Y").build();
+ AddressData addressData3 = AddressData.builder().setLanguageCode("Y").build();
+
+ assertThat(addressData1).isNotEqualTo(addressData2);
+ assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+ assertThat(addressData2).isEqualTo(addressData3);
+ assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+ }
}
diff --git a/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java b/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
index f89debf..32e8e79 100644
--- a/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
+++ b/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
@@ -17,6 +17,8 @@
package com.google.i18n.addressinput.common;
import static com.google.common.truth.Truth.assertThat;
+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.POSTAL_CODE;
@@ -34,7 +36,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@@ -259,4 +261,52 @@
POSTAL_CODE, MISSING_REQUIRED_FIELD),
verify(address).getProblems());
}
+
+ @Test public void testVerifyCountryOnly_Valid() {
+ AddressData address = AddressData.builder()
+ .setCountry("US")
+ .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+ .build();
+ AddressProblems problems = new AddressProblems();
+ verifierFor(StandardChecks.PROBLEM_MAP)
+ .verifyFields(address, problems, EnumSet.of(COUNTRY));
+ assertThat(problems.getProblems()).isEmpty();
+ }
+
+ @Test public void testVerifyCountryOnly_InvalidCountry() {
+ AddressData address = AddressData.builder()
+ .setCountry("USA")
+ .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+ .build();
+ AddressProblems problems = new AddressProblems();
+ verifierFor(StandardChecks.PROBLEM_MAP)
+ .verifyFields(address, problems, EnumSet.of(COUNTRY));
+ assertThat(problems.getProblem(COUNTRY)).isEqualTo(UNKNOWN_VALUE);
+ assertThat(problems.getProblem(ADMIN_AREA)).isNull();
+ }
+
+ @Test public void testVerifyCountryAndPostalCodeOnly_Valid() {
+ AddressData address = AddressData.builder()
+ .setCountry("US")
+ .setPostalCode("94043")
+ .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+ .build();
+ AddressProblems problems = new AddressProblems();
+ verifierFor(StandardChecks.PROBLEM_MAP)
+ .verifyFields(address, problems, EnumSet.of(COUNTRY, POSTAL_CODE));
+ assertThat(problems.getProblems()).isEmpty();
+ }
+
+ @Test public void testVerifyCountryAndPostalCodeOnly_InvalidPostalCode() {
+ AddressData address = AddressData.builder()
+ .setCountry("US")
+ .setPostalCode("094043")
+ .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+ .build();
+ AddressProblems problems = new AddressProblems();
+ verifierFor(StandardChecks.PROBLEM_MAP)
+ .verifyFields(address, problems, EnumSet.of(COUNTRY, POSTAL_CODE));
+ assertThat(problems.getProblem(POSTAL_CODE)).isEqualTo(INVALID_FORMAT);
+ assertThat(problems.getProblem(ADMIN_AREA)).isNull();
+ }
}
diff --git a/cpp/README b/cpp/README.md
similarity index 97%
rename from cpp/README
rename to cpp/README.md
index d6fbf44..37050d0 100644
--- a/cpp/README
+++ b/cpp/README.md
@@ -1,5 +1,4 @@
-Intro
-=====
+# Intro
The C++ version of libaddressinput library provides UI layout information and
validation for address input forms.
@@ -15,8 +14,7 @@
and include directories in libaddressinput.gypi to link with your own
third-party libraries.
-Dependencies
-============
+# Dependencies
The library depends on these tools and libraries:
@@ -59,8 +57,7 @@
http://python.org/
https://code.google.com/p/re2/
-Build
-=====
+# Build
Building the library involves generating an out/Default/build.ninja file and
running ninja:
@@ -74,8 +71,7 @@
$ export GYP_DEFINES="gtest_dir='/xxx/include' gtest_src_dir='/xxx'"
-Test
-====
+# Test
This command will execute the unit tests for the library:
diff --git a/cpp/include/libaddressinput/util/basictypes.h b/cpp/include/libaddressinput/util/basictypes.h
index 663133f..4436f5a 100644
--- a/cpp/include/libaddressinput/util/basictypes.h
+++ b/cpp/include/libaddressinput/util/basictypes.h
@@ -108,9 +108,16 @@
// A macro to disallow the copy constructor and operator= functions
// This should be used in the private: declarations for a class
#if !defined(DISALLOW_COPY_AND_ASSIGN)
+#if __cplusplus >= 201103L
+// Use C++11 deleted destructor since they provide better error messages.
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+ TypeName(const TypeName&) = delete; \
+ TypeName& operator=(const TypeName&) = delete
+#else // Not C++11
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
+#endif // Not C++11
#endif
// The arraysize(arr) macro returns the # of elements in an array arr.
@@ -120,7 +127,7 @@
//
// One caveat is that arraysize() doesn't accept any array of an
// anonymous type or a type defined inside a function. In these rare
-// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is
+// cases, you have to use the unsafe ARRAYSIZE() macro below. This is
// due to a limitation in C++'s template system. The limitation might
// eventually be removed, but it hasn't happened yet.
@@ -142,26 +149,26 @@
#define arraysize(array) (sizeof(ArraySizeHelper(array)))
#endif
-// ARRAYSIZE_UNSAFE performs essentially the same calculation as arraysize,
+// ARRAYSIZE performs essentially the same calculation as arraysize,
// but can be used on anonymous types or types defined inside
// functions. It's less safe than arraysize as it accepts some
// (although not all) pointers. Therefore, you should use arraysize
// whenever possible.
//
-// The expression ARRAYSIZE_UNSAFE(a) is a compile-time constant of type
+// The expression ARRAYSIZE(a) is a compile-time constant of type
// size_t.
//
-// ARRAYSIZE_UNSAFE catches a few type errors. If you see a compiler error
+// ARRAYSIZE catches a few type errors. If you see a compiler error
//
// "warning: division by zero in ..."
//
-// when using ARRAYSIZE_UNSAFE, you are (wrongfully) giving it a pointer.
-// You should only use ARRAYSIZE_UNSAFE on statically allocated arrays.
+// when using ARRAYSIZE, you are (wrongfully) giving it a pointer.
+// You should only use ARRAYSIZE on statically allocated arrays.
//
// The following comments are on the implementation details, and can
// be ignored by the users.
//
-// ARRAYSIZE_UNSAFE(arr) works by inspecting sizeof(arr) (the # of bytes in
+// ARRAYSIZE(arr) works by inspecting sizeof(arr) (the # of bytes in
// the array) and sizeof(*(arr)) (the # of bytes in one array
// element). If the former is divisible by the latter, perhaps arr is
// indeed an array, in which case the division result is the # of
@@ -179,8 +186,8 @@
// where a pointer is 4 bytes, this means all pointers to a type whose
// size is 3 or greater than 4 will be (righteously) rejected.
-#if !defined(ARRAYSIZE_UNSAFE)
-#define ARRAYSIZE_UNSAFE(a) \
+#if !defined(ARRAYSIZE)
+#define ARRAYSIZE(a) \
((sizeof(a) / sizeof(*(a))) / \
static_cast<size_t>(!(sizeof(a) % sizeof(*(a)))))
#endif
@@ -189,7 +196,7 @@
// expression is true. For example, you could use it to verify the
// size of a static array:
//
-// COMPILE_ASSERT(ARRAYSIZE_UNSAFE(content_type_names) == CONTENT_NUM_TYPES,
+// COMPILE_ASSERT(ARRAYSIZE(content_type_names) == CONTENT_NUM_TYPES,
// content_type_names_incorrect_size);
//
// or to make sure a struct is smaller than a certain size:
@@ -200,14 +207,23 @@
// the expression is false, most compilers will issue a warning/error
// containing the name of the variable.
+#if !defined(COMPILE_ASSERT)
+
+#if __cplusplus >= 201103L
+// Use static_assert() directly when using a C++11 compiler.
+// This provides human-friendly error messages.
+#define COMPILE_ASSERT(expr, msg) static_assert((expr), #msg)
+#else // Not C++11
+// Otherwise, use a compile-time type error.
template <bool>
struct CompileAssert {
};
-#if !defined(COMPILE_ASSERT)
#define COMPILE_ASSERT(expr, msg) \
typedef CompileAssert<(bool(expr))> msg[bool(expr) ? 1 : -1]
-#endif
+
+#endif // Not C++11
+#endif // !defined(COMPILE_ASSERT)
#endif // I18N_ADDRESSINPUT_UTIL_BASICTYPES_H_
#endif // I18N_ADDRESSINPUT_USE_BASICTYPES_OVERRIDE
diff --git a/cpp/src/address_field_util.cc b/cpp/src/address_field_util.cc
index 26de3c0..3c2b7cd 100644
--- a/cpp/src/address_field_util.cc
+++ b/cpp/src/address_field_util.cc
@@ -15,13 +15,12 @@
#include "address_field_util.h"
#include <libaddressinput/address_field.h>
+#include <libaddressinput/util/basictypes.h>
#include <algorithm>
#include <cassert>
#include <cstddef>
-#include <map>
#include <string>
-#include <utility>
#include <vector>
#include "format_element.h"
@@ -31,33 +30,33 @@
namespace {
-std::map<char, AddressField> InitFields() {
- std::map<char, AddressField> fields;
- fields.insert(std::make_pair('R', COUNTRY));
- fields.insert(std::make_pair('S', ADMIN_AREA));
- fields.insert(std::make_pair('C', LOCALITY));
- fields.insert(std::make_pair('D', DEPENDENT_LOCALITY));
- fields.insert(std::make_pair('X', SORTING_CODE));
- fields.insert(std::make_pair('Z', POSTAL_CODE));
- fields.insert(std::make_pair('A', STREET_ADDRESS));
- fields.insert(std::make_pair('O', ORGANIZATION));
- fields.insert(std::make_pair('N', RECIPIENT));
- return fields;
-}
+// Check whether |c| is a field token character. On success, return true
+// and sets |*field| to the corresponding AddressField value. Return false
+// on failure.
+bool ParseFieldToken(char c, AddressField* field) {
+ assert(field != NULL);
-const std::map<char, AddressField>& GetFields() {
- static const std::map<char, AddressField> kFields(InitFields());
- return kFields;
-}
+ // Simple mapping from field token characters to AddressField values.
+ static const struct Entry { char ch; AddressField field; } kTokenMap[] = {
+ { 'R', COUNTRY },
+ { 'S', ADMIN_AREA },
+ { 'C', LOCALITY },
+ { 'D', DEPENDENT_LOCALITY },
+ { 'X', SORTING_CODE },
+ { 'Z', POSTAL_CODE },
+ { 'A', STREET_ADDRESS },
+ { 'O', ORGANIZATION },
+ { 'N', RECIPIENT },
+ };
+ const size_t kTokenMapSize = arraysize(kTokenMap);
-bool IsFieldToken(char c) {
- return GetFields().find(c) != GetFields().end();
-}
-
-AddressField ParseFieldToken(char c) {
- std::map<char, AddressField>::const_iterator it = GetFields().find(c);
- assert(it != GetFields().end());
- return it->second;
+ for (size_t n = 0; n < kTokenMapSize; ++n) {
+ if (c == kTokenMap[n].ch) {
+ *field = kTokenMap[n].field;
+ return true;
+ }
+ }
+ return false;
}
} // namespace
@@ -85,10 +84,11 @@
break;
}
// Process the token after the %.
+ AddressField field;
if (*next == 'n') {
elements->push_back(FormatElement());
- } else if (IsFieldToken(*next)) {
- elements->push_back(FormatElement(ParseFieldToken(*next)));
+ } else if (ParseFieldToken(*next, &field)) {
+ elements->push_back(FormatElement(field));
} // Else it's an unknown token, we ignore it.
}
// Push back any trailing literal.
@@ -103,8 +103,9 @@
fields->clear();
for (std::string::const_iterator it = required.begin();
it != required.end(); ++it) {
- if (IsFieldToken(*it)) {
- fields->push_back(ParseFieldToken(*it));
+ AddressField field;
+ if (ParseFieldToken(*it, &field)) {
+ fields->push_back(field);
}
}
}
diff --git a/cpp/src/region_data_constants.cc b/cpp/src/region_data_constants.cc
index ca1ee1b..738cd07 100644
--- a/cpp/src/region_data_constants.cc
+++ b/cpp/src/region_data_constants.cc
@@ -58,8 +58,7 @@
region_data.insert(std::make_pair("AF", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
"\"zipex\":\"1001,2601,3801\","
- "\"posturl\":\"http://afghanpost.gov.af/Postal%20Code/\","
- "\"languages\":\"fa~ps\""
+ "\"languages\":\"fa~ps~uz-Arab~tk~bal\""
"}"));
region_data.insert(std::make_pair("AG", "{"
"\"require\":\"A\","
@@ -106,7 +105,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"1010,3741\","
"\"posturl\":\"http://www.post.at/post_subsite_postleitzahlfinder.php\","
- "\"languages\":\"de\""
+ "\"languages\":\"de~hr~sl~hu\""
"}"));
region_data.insert(std::make_pair("AU", "{"
"\"fmt\":\"%O%n%N%n%A%n%C %S %Z\","
@@ -130,12 +129,12 @@
region_data.insert(std::make_pair("AZ", "{"
"\"fmt\":\"%N%n%O%n%A%nAZ %Z %C\","
"\"zipex\":\"1000\","
- "\"languages\":\"az-Latn~az-Cyrl\""
+ "\"languages\":\"az~az-Cyrl\""
"}"));
region_data.insert(std::make_pair("BA", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"71000\","
- "\"languages\":\"bs-Cyrl~bs-Latn~hr~sr-Cyrl~sr-Latn\""
+ "\"languages\":\"bs~bs-Cyrl~hr~sr~sr-Latn\""
"}"));
region_data.insert(std::make_pair("BB", "{"
"\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\","
@@ -193,8 +192,8 @@
region_data.insert(std::make_pair("BN", "{"
"\"fmt\":\"%N%n%O%n%A%n%C %Z\","
"\"zipex\":\"BT2328,KA1131,BA1511\","
- "\"posturl\":\"http://www.post.gov.bn/index.php/extensions/postcode-guide\","
- "\"languages\":\"ms-Latn~ms-Arab\""
+ "\"posturl\":\"http://www.post.gov.bn/SitePages/postcodes.aspx\","
+ "\"languages\":\"ms~ms-Arab\""
"}"));
region_data.insert(std::make_pair("BO", "{"
"\"languages\":\"es~qu~ay\""
@@ -240,7 +239,7 @@
"\"fmt\":\"%N%n%O%n%A%n%C %S %Z\","
"\"require\":\"ACSZ\","
"\"zipex\":\"H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1\","
- "\"posturl\":\"http://www.canadapost.ca/cpotools/apps/fpc/personal/findByCity\?execution=e2s1\","
+ "\"posturl\":\"https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf\","
"\"languages\":\"en~fr\""
"}"));
region_data.insert(std::make_pair("CC", "{"
@@ -249,7 +248,7 @@
"\"languages\":\"en\""
"}"));
region_data.insert(std::make_pair("CD", "{"
- "\"languages\":\"fr\""
+ "\"languages\":\"sw~lua~fr~ln~kg\""
"}"));
region_data.insert(std::make_pair("CF", "{"
"\"languages\":\"fr~sg\""
@@ -262,7 +261,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"2544,1211,1556,3030\","
"\"posturl\":\"http://www.post.ch/db/owa/pv_plz_pack/pr_main\","
- "\"languages\":\"de~gsw~fr~it\""
+ "\"languages\":\"de~gsw~fr~it~rm\""
"}"));
region_data.insert(std::make_pair("CI", "{"
"\"fmt\":\"%N%n%O%n%X %A %C %X\","
@@ -344,7 +343,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"8660,1566\","
"\"posturl\":\"http://www.postdanmark.dk/da/Privat/Kundeservice/postnummerkort/Sider/Find-postnummer.aspx\","
- "\"languages\":\"da\""
+ "\"languages\":\"da~de~kl\""
"}"));
region_data.insert(std::make_pair("DM", "{"
"\"languages\":\"en\""
@@ -391,7 +390,7 @@
"\"require\":\"ACSZ\","
"\"zipex\":\"28039,28300,28070\","
"\"posturl\":\"http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp\","
- "\"languages\":\"es\""
+ "\"languages\":\"es~ca~gl~eu\""
"}"));
region_data.insert(std::make_pair("ET", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -406,7 +405,7 @@
"\"languages\":\"fi~sv\""
"}"));
region_data.insert(std::make_pair("FJ", "{"
- "\"languages\":\"en~hif-Latn~fj\""
+ "\"languages\":\"en~hif~fj\""
"}"));
region_data.insert(std::make_pair("FK", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -445,7 +444,7 @@
"\"locality_name_type\":\"post_town\","
"\"zipex\":\"EC1Y 8SY,GIR 0AA,M2 5BQ,M34 4AB,CR0 2YR,DN16 9AA,W1A 4ZZ,EC1A 1HQ,OX14 4PG,BS18 8HF,NR25 7HG,RH6 0NP,BH23 6AA,B6 5BA,SO23 9AP,PO1 3AX,BFPO 61\","
"\"posturl\":\"http://www.royalmail.com/postcode-finder\","
- "\"languages\":\"en\""
+ "\"languages\":\"en~cy~gd~ga\""
"}"));
region_data.insert(std::make_pair("GD", "{"
"\"languages\":\"en\""
@@ -454,7 +453,7 @@
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"0101\","
"\"posturl\":\"http://www.georgianpost.ge/index.php\?page=10\","
- "\"languages\":\"ka\""
+ "\"languages\":\"ka~ab~os\""
"}"));
region_data.insert(std::make_pair("GF", "{"
"\"fmt\":\"%O%n%N%n%A%n%Z %C %X\","
@@ -471,7 +470,7 @@
"\"languages\":\"en\""
"}"));
region_data.insert(std::make_pair("GH", "{"
- "\"languages\":\"en\""
+ "\"languages\":\"ak~en~ee~gaa\""
"}"));
region_data.insert(std::make_pair("GI", "{"
"\"fmt\":\"%N%n%O%n%A%nGIBRALTAR%n%Z\","
@@ -518,13 +517,12 @@
region_data.insert(std::make_pair("GT", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z- %C\","
"\"zipex\":\"09001,01501\","
- "\"languages\":\"es\""
+ "\"languages\":\"es~quc\""
"}"));
region_data.insert(std::make_pair("GU", "{"
- "\"fmt\":\"%N%n%O%n%A%n%C %S %Z\","
- "\"require\":\"ACSZ\","
+ "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
+ "\"require\":\"ACZ\","
"\"zip_name_type\":\"zip\","
- "\"state_name_type\":\"state\","
"\"zipex\":\"96910,96931\","
"\"posturl\":\"http://zip4.usps.com/zip4/welcome.jsp\","
"\"languages\":\"en~ch\""
@@ -559,7 +557,7 @@
"\"fmt\":\"%N%n%O%n%A%nHR-%Z %C\","
"\"zipex\":\"10000,21001,10002\","
"\"posturl\":\"http://www.posta.hr/default.aspx\?pretpum\","
- "\"languages\":\"hr\""
+ "\"languages\":\"hr~it\""
"}"));
region_data.insert(std::make_pair("HT", "{"
"\"fmt\":\"%N%n%O%n%A%nHT%Z %C\","
@@ -607,8 +605,8 @@
"\"zip_name_type\":\"pin\","
"\"state_name_type\":\"state\","
"\"zipex\":\"110034,110001\","
- "\"posturl\":\"http://cept.gov.in/lbpsd/placesearch.aspx\","
- "\"languages\":\"en\""
+ "\"posturl\":\"https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx\","
+ "\"languages\":\"en~hi\""
"}"));
region_data.insert(std::make_pair("IO", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -620,7 +618,7 @@
"\"fmt\":\"%O%n%N%n%A%n%C, %S%n%Z\","
"\"require\":\"ACS\","
"\"zipex\":\"31001\","
- "\"languages\":\"ar\""
+ "\"languages\":\"ar~ckb~az-Arab\""
"}"));
region_data.insert(std::make_pair("IR", "{"
"\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\","
@@ -631,7 +629,7 @@
region_data.insert(std::make_pair("IS", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"320,121,220,110\","
- "\"posturl\":\"https://www.postur.is/um-postinn/posthus/gotuskra/\","
+ "\"posturl\":\"http://www.postur.is/einstaklingar/posthus/postnumer/\","
"\"languages\":\"is\""
"}"));
region_data.insert(std::make_pair("IT", "{"
@@ -676,7 +674,7 @@
region_data.insert(std::make_pair("KG", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"720001\","
- "\"languages\":\"ky-Cyrl~ru\""
+ "\"languages\":\"ky~ru\""
"}"));
region_data.insert(std::make_pair("KH", "{"
"\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -723,7 +721,7 @@
region_data.insert(std::make_pair("KZ", "{"
"\"fmt\":\"%Z%n%S%n%C%n%A%n%O%n%N\","
"\"zipex\":\"040900,050012\","
- "\"languages\":\"ru~kk-Cyrl\""
+ "\"languages\":\"ru~kk\""
"}"));
region_data.insert(std::make_pair("LA", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -786,7 +784,7 @@
region_data.insert(std::make_pair("MA", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"53000,10000,20050,16052\","
- "\"languages\":\"ar~fr~tzm-Latn\""
+ "\"languages\":\"ar~fr~tzm\""
"}"));
region_data.insert(std::make_pair("MC", "{"
"\"fmt\":\"%N%n%O%n%A%nMC-%Z %C %X\","
@@ -827,7 +825,7 @@
region_data.insert(std::make_pair("MK", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"1314,1321,1443,1062\","
- "\"languages\":\"mk\""
+ "\"languages\":\"mk~sq\""
"}"));
region_data.insert(std::make_pair("ML", "{"
"\"languages\":\"fr\""
@@ -841,7 +839,7 @@
"\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\","
"\"zipex\":\"65030,65270\","
"\"posturl\":\"http://www.zipcode.mn/\","
- "\"languages\":\"mn-Cyrl\""
+ "\"languages\":\"mn\""
"}"));
region_data.insert(std::make_pair("MO", "{"
"\"fmt\":\"%A%n%O%n%N\","
@@ -936,7 +934,7 @@
"\"languages\":\"en\""
"}"));
region_data.insert(std::make_pair("NG", "{"
- "\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\","
+ "\"fmt\":\"%N%n%O%n%A%n%D%n%C %Z%n%S\","
"\"state_name_type\":\"state\","
"\"zipex\":\"930283,300001,931104\","
"\"posturl\":\"http://www.nigeriapostcodes.com/\","
@@ -954,7 +952,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"1234 AB,2490 AA\","
"\"posturl\":\"http://www.postnl.nl/voorthuis/\","
- "\"languages\":\"nl\""
+ "\"languages\":\"nl~fy\""
"}"));
region_data.insert(std::make_pair("NO", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -962,7 +960,7 @@
"\"locality_name_type\":\"post_town\","
"\"zipex\":\"0025,0107,6631\","
"\"posturl\":\"http://adressesok.posten.no/nb/postal_codes/search\","
- "\"languages\":\"no~nn\""
+ "\"languages\":\"no~nn~se\""
"}"));
region_data.insert(std::make_pair("NP", "{"
"\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -1031,7 +1029,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"00-950,05-470,48-300,32-015,00-940\","
"\"posturl\":\"http://kody.poczta-polska.pl/\","
- "\"languages\":\"pl\""
+ "\"languages\":\"pl~de~csb~lt\""
"}"));
region_data.insert(std::make_pair("PM", "{"
"\"fmt\":\"%O%n%N%n%A%n%Z %C %X\","
@@ -1097,7 +1095,7 @@
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"106314\","
"\"posturl\":\"http://www.posta.rs/struktura/lat/aplikacije/pronadji/nadji-postu.asp\","
- "\"languages\":\"sr-Cyrl~sr-Latn\""
+ "\"languages\":\"sr~sr-Latn~hu~ro~hr~sk~uk\""
"}"));
region_data.insert(std::make_pair("RU", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\","
@@ -1130,14 +1128,14 @@
"\"locality_name_type\":\"post_town\","
"\"zipex\":\"11455,12345,10500\","
"\"posturl\":\"http://www.posten.se/sv/Kundservice/Sidor/Sok-postnummer-resultat.aspx\","
- "\"languages\":\"sv\""
+ "\"languages\":\"sv~fi\""
"}"));
region_data.insert(std::make_pair("SG", "{"
"\"fmt\":\"%N%n%O%n%A%nSINGAPORE %Z\","
"\"require\":\"AZ\","
"\"zipex\":\"546080,308125,408600\","
"\"posturl\":\"https://www.singpost.com/find-postal-code\","
- "\"languages\":\"en~zh-Hans~ms-Latn~ta\""
+ "\"languages\":\"en~zh~ms~ta\""
"}"));
region_data.insert(std::make_pair("SH", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -1177,7 +1175,7 @@
region_data.insert(std::make_pair("SN", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"12500,46024,16556,10000\","
- "\"languages\":\"wo~fr\""
+ "\"languages\":\"wo~fr~ff~srr~dyo~sav~mfv~bjt~snf~knf~bsc~mey~tnr\""
"}"));
region_data.insert(std::make_pair("SO", "{"
"\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\","
@@ -1207,7 +1205,7 @@
region_data.insert(std::make_pair("SZ", "{"
"\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
"\"zipex\":\"H100\","
- "\"posturl\":\"http://www.sptc.co.sz/swazipost/codes.php\","
+ "\"posturl\":\"http://www.sptc.co.sz/swazipost/codes/index.php\","
"\"languages\":\"en~ss\""
"}"));
region_data.insert(std::make_pair("TA", "{"
@@ -1239,7 +1237,7 @@
region_data.insert(std::make_pair("TJ", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"735450,734025\","
- "\"languages\":\"tg-Cyrl\""
+ "\"languages\":\"tg\""
"}"));
region_data.insert(std::make_pair("TK", "{"
"\"languages\":\"en~tkl\""
@@ -1250,7 +1248,7 @@
region_data.insert(std::make_pair("TM", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"744000\","
- "\"languages\":\"tk-Latn\""
+ "\"languages\":\"tk\""
"}"));
region_data.insert(std::make_pair("TN", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -1331,12 +1329,12 @@
"\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\","
"\"zipex\":\"702100,700000\","
"\"posturl\":\"http://www.pochta.uz/ru/uslugi/indexsearch.html\","
- "\"languages\":\"uz-Latn~uz-Cyrl\""
+ "\"languages\":\"uz~uz-Cyrl\""
"}"));
region_data.insert(std::make_pair("VA", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"00120\","
- "\"languages\":\"it~la\""
+ "\"languages\":\"it\""
"}"));
region_data.insert(std::make_pair("VC", "{"
"\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -1389,7 +1387,7 @@
region_data.insert(std::make_pair("XK", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
"\"zipex\":\"10000\","
- "\"languages\":\"sq~sr-Cyrl~sr-Latn\""
+ "\"languages\":\"sq~sr~sr-Latn\""
"}"));
region_data.insert(std::make_pair("YE", "{"
"\"languages\":\"ar\""
@@ -1405,7 +1403,7 @@
"\"require\":\"ACZ\","
"\"zipex\":\"0083,1451,0001\","
"\"posturl\":\"https://www.postoffice.co.za/contactus/postalcode.html\","
- "\"languages\":\"en\""
+ "\"languages\":\"en~zu~xh~af~nso~tn~st~ts~ss~ve~nr\""
"}"));
region_data.insert(std::make_pair("ZM", "{"
"\"fmt\":\"%N%n%O%n%A%n%Z %C\","
diff --git a/cpp/src/rule.cc b/cpp/src/rule.cc
index 550ac2e..91427d0 100644
--- a/cpp/src/rule.cc
+++ b/cpp/src/rule.cc
@@ -14,9 +14,9 @@
#include "rule.h"
+#include <algorithm>
#include <cassert>
#include <cstddef>
-#include <map>
#include <string>
#include <utility>
@@ -36,110 +36,123 @@
namespace {
-typedef std::map<std::string, int> NameMessageIdMap;
-
// Used as a separator in a list of items. For example, the list of supported
// languages can be "de~fr~it".
const char kSeparator = '~';
-NameMessageIdMap InitAdminAreaMessageIds() {
- NameMessageIdMap message_ids;
- message_ids.insert(std::make_pair(
- "area", IDS_LIBADDRESSINPUT_AREA));
- message_ids.insert(std::make_pair(
- "county", IDS_LIBADDRESSINPUT_COUNTY));
- message_ids.insert(std::make_pair(
- "department", IDS_LIBADDRESSINPUT_DEPARTMENT));
- message_ids.insert(std::make_pair(
- "district", IDS_LIBADDRESSINPUT_DISTRICT));
- message_ids.insert(std::make_pair(
- "do_si", IDS_LIBADDRESSINPUT_DO_SI));
- message_ids.insert(std::make_pair(
- "emirate", IDS_LIBADDRESSINPUT_EMIRATE));
- message_ids.insert(std::make_pair(
- "island", IDS_LIBADDRESSINPUT_ISLAND));
- message_ids.insert(std::make_pair(
- "oblast", IDS_LIBADDRESSINPUT_OBLAST));
- message_ids.insert(std::make_pair(
- "parish", IDS_LIBADDRESSINPUT_PARISH));
- message_ids.insert(std::make_pair(
- "prefecture", IDS_LIBADDRESSINPUT_PREFECTURE));
- message_ids.insert(std::make_pair(
- "province", IDS_LIBADDRESSINPUT_PROVINCE));
- message_ids.insert(std::make_pair(
- "state", IDS_LIBADDRESSINPUT_STATE));
- return message_ids;
-}
+// NameIdMap is a convenience POD struct that implements a mapping from
+// names to message ids, with sorted arrays of NameIdInfo entries.
+struct NameIdInfo {
+ const char* name;
+ int id;
-const NameMessageIdMap& GetAdminAreaMessageIds() {
- static const NameMessageIdMap kAdminAreaMessageIds(InitAdminAreaMessageIds());
- return kAdminAreaMessageIds;
-}
+ static bool less(const NameIdInfo& a, const NameIdInfo& b) {
+ return strcmp(a.name, b.name) < 0;
+ }
+};
-NameMessageIdMap InitPostalCodeMessageIds() {
- NameMessageIdMap message_ids;
- message_ids.insert(std::make_pair(
- "eircode", IDS_LIBADDRESSINPUT_EIR_CODE_LABEL));
- message_ids.insert(std::make_pair(
- "pin", IDS_LIBADDRESSINPUT_PIN_CODE_LABEL));
- message_ids.insert(std::make_pair(
- "postal", IDS_LIBADDRESSINPUT_POSTAL_CODE_LABEL));
- message_ids.insert(std::make_pair(
- "zip", IDS_LIBADDRESSINPUT_ZIP_CODE_LABEL));
- return message_ids;
-}
+struct NameIdMap {
+ const NameIdInfo* infos;
+ size_t size;
-const NameMessageIdMap& GetPostalCodeMessageIds() {
- static const NameMessageIdMap kPostalCodeMessageIds(
- InitPostalCodeMessageIds());
- return kPostalCodeMessageIds;
-}
+ // Return the message id corresponding to |name|, ir INVALID_MESSAGE_ID
+ // if it is not found in the map.
+ int GetIdFromName(const std::string& name) const {
+ NameIdInfo key = { name.c_str() };
+ const NameIdInfo* begin = infos;
+ const NameIdInfo* end = begin + size;
+ const NameIdInfo* probe =
+ std::lower_bound(begin, end, key, NameIdInfo::less);
+ return (probe != end && name == probe->name)
+ ? probe->id : INVALID_MESSAGE_ID;
+ }
-NameMessageIdMap InitLocalityMessageIds() {
- NameMessageIdMap message_ids;
- message_ids.insert(std::make_pair(
- "city", IDS_LIBADDRESSINPUT_LOCALITY_LABEL));
- message_ids.insert(std::make_pair(
- "district", IDS_LIBADDRESSINPUT_DISTRICT));
- message_ids.insert(std::make_pair(
- "post_town", IDS_LIBADDRESSINPUT_POST_TOWN));
- message_ids.insert(std::make_pair(
- "suburb", IDS_LIBADDRESSINPUT_SUBURB));
- return message_ids;
-}
+ // Return true iff the map is properly sorted.
+ bool IsSorted() const {
+ for (size_t n = 1; n < size; ++n) {
+ if (!NameIdInfo::less(infos[n - 1], infos[n])) {
+ return false;
+ }
+ }
+ return true;
+ }
+};
-const NameMessageIdMap& GetLocalityMessageIds() {
- static const NameMessageIdMap kLocalityMessageIds(
- InitLocalityMessageIds());
- return kLocalityMessageIds;
-}
+const NameIdInfo kAdminAreaInfoArray[] = {
+ {"area", IDS_LIBADDRESSINPUT_AREA},
+ {"county", IDS_LIBADDRESSINPUT_COUNTY},
+ {"department", IDS_LIBADDRESSINPUT_DEPARTMENT},
+ {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+ {"do_si", IDS_LIBADDRESSINPUT_DO_SI},
+ {"emirate", IDS_LIBADDRESSINPUT_EMIRATE},
+ {"island", IDS_LIBADDRESSINPUT_ISLAND},
+ {"oblast", IDS_LIBADDRESSINPUT_OBLAST},
+ {"parish", IDS_LIBADDRESSINPUT_PARISH},
+ {"prefecture", IDS_LIBADDRESSINPUT_PREFECTURE},
+ {"province", IDS_LIBADDRESSINPUT_PROVINCE},
+ {"state", IDS_LIBADDRESSINPUT_STATE},
+};
-NameMessageIdMap InitSublocalityMessageIds() {
- NameMessageIdMap message_ids;
- message_ids.insert(std::make_pair(
- "suburb", IDS_LIBADDRESSINPUT_SUBURB));
- message_ids.insert(std::make_pair(
- "district", IDS_LIBADDRESSINPUT_DISTRICT));
- message_ids.insert(std::make_pair(
- "neighborhood", IDS_LIBADDRESSINPUT_NEIGHBORHOOD));
- message_ids.insert(std::make_pair(
- "townland", IDS_LIBADDRESSINPUT_TOWNLAND));
- message_ids.insert(std::make_pair(
- "village_township", IDS_LIBADDRESSINPUT_VILLAGE_TOWNSHIP));
- return message_ids;
-}
+const NameIdMap kAdminAreaMessageIds = {
+ kAdminAreaInfoArray,
+ arraysize(kAdminAreaInfoArray)
+};
-const NameMessageIdMap& GetSublocalityMessageIds() {
- static const NameMessageIdMap kSublocalityMessageIds(
- InitSublocalityMessageIds());
- return kSublocalityMessageIds;
-}
+const NameIdInfo kPostalCodeInfoArray[] = {
+ {"eircode", IDS_LIBADDRESSINPUT_EIR_CODE_LABEL},
+ {"pin", IDS_LIBADDRESSINPUT_PIN_CODE_LABEL},
+ {"postal", IDS_LIBADDRESSINPUT_POSTAL_CODE_LABEL},
+ {"zip", IDS_LIBADDRESSINPUT_ZIP_CODE_LABEL},
+};
-int GetMessageIdFromName(const std::string& name,
- const NameMessageIdMap& message_ids) {
- NameMessageIdMap::const_iterator it = message_ids.find(name);
- return it != message_ids.end() ? it->second : INVALID_MESSAGE_ID;
-}
+const NameIdMap kPostalCodeMessageIds = {
+ kPostalCodeInfoArray,
+ arraysize(kPostalCodeInfoArray),
+};
+
+const NameIdInfo kLocalityInfoArray[] = {
+ {"city", IDS_LIBADDRESSINPUT_LOCALITY_LABEL},
+ {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+ {"post_town", IDS_LIBADDRESSINPUT_POST_TOWN},
+ {"suburb", IDS_LIBADDRESSINPUT_SUBURB},
+};
+
+const NameIdMap kLocalityMessageIds = {
+ kLocalityInfoArray,
+ arraysize(kLocalityInfoArray),
+};
+
+const NameIdInfo kSublocalityInfoArray[] = {
+ {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+ {"neighborhood", IDS_LIBADDRESSINPUT_NEIGHBORHOOD},
+ {"suburb", IDS_LIBADDRESSINPUT_SUBURB},
+ {"townland", IDS_LIBADDRESSINPUT_TOWNLAND},
+ {"village_township", IDS_LIBADDRESSINPUT_VILLAGE_TOWNSHIP},
+};
+
+const NameIdMap kSublocalityMessageIds = {
+ kSublocalityInfoArray,
+ arraysize(kSublocalityInfoArray),
+};
+
+#ifndef _NDEBUG
+// Helper type used to check that all maps are sorted at runtime.
+// Should be used as a local static variable to ensure this is checked only
+// once per process. Usage is simply:
+//
+// ... someFunction(....) {
+// static StaticMapChecker map_checker;
+// ... anything else ...
+// }
+struct StaticMapChecker {
+ StaticMapChecker() {
+ assert(kAdminAreaMessageIds.IsSorted());
+ assert(kPostalCodeMessageIds.IsSorted());
+ assert(kLocalityMessageIds.IsSorted());
+ assert(kSublocalityMessageIds.IsSorted());
+ }
+};
+#endif // _NDEBUG
// Determines whether a given string is a reg-exp or a string. We consider a
// string to be anything that doesn't contain characters with special meanings
@@ -217,6 +230,11 @@
}
void Rule::ParseJsonRule(const Json& json) {
+#ifndef _NDEBUG
+ // Don't remove, see StaticMapChecker comments above.
+ static StaticMapChecker map_checker;
+ #endif // !_NDEBUG
+
std::string value;
if (json.GetStringValueForKey("id", &value)) {
id_.swap(value);
@@ -272,23 +290,19 @@
}
if (json.GetStringValueForKey("state_name_type", &value)) {
- admin_area_name_message_id_ =
- GetMessageIdFromName(value, GetAdminAreaMessageIds());
+ admin_area_name_message_id_ = kAdminAreaMessageIds.GetIdFromName(value);
}
if (json.GetStringValueForKey("zip_name_type", &value)) {
- postal_code_name_message_id_ =
- GetMessageIdFromName(value, GetPostalCodeMessageIds());
+ postal_code_name_message_id_ = kPostalCodeMessageIds.GetIdFromName(value);
}
if (json.GetStringValueForKey("locality_name_type", &value)) {
- locality_name_message_id_ =
- GetMessageIdFromName(value, GetLocalityMessageIds());
+ locality_name_message_id_ = kLocalityMessageIds.GetIdFromName(value);
}
if (json.GetStringValueForKey("sublocality_name_type", &value)) {
- sublocality_name_message_id_ =
- GetMessageIdFromName(value, GetSublocalityMessageIds());
+ sublocality_name_message_id_ = kSublocalityMessageIds.GetIdFromName(value);
}
if (json.GetStringValueForKey("name", &value)) {