1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.i18n.addressinput;
18
19import android.app.ProgressDialog;
20import android.content.Context;
21import android.os.Handler;
22import android.telephony.TelephonyManager;
23import android.util.Log;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.AdapterView;
28import android.widget.ArrayAdapter;
29import android.widget.EditText;
30import android.widget.LinearLayout;
31import android.widget.LinearLayout.LayoutParams;
32import android.widget.Spinner;
33import android.widget.TextView;
34
35import com.android.i18n.addressinput.AddressField.WidthType;
36import com.android.i18n.addressinput.AddressUiComponent.UiComponent;
37import com.android.i18n.addressinput.LookupKey.KeyType;
38import com.android.i18n.addressinput.LookupKey.ScriptType;
39
40import java.text.Collator;
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.EnumMap;
44import java.util.HashMap;
45import java.util.List;
46import java.util.Locale;
47import java.util.Map;
48
49/**
50 * Address widget that lays out address fields, validate and format addresses according to local
51 * customs.
52 */
53public class AddressWidget implements AdapterView.OnItemSelectedListener {
54    private Context mContext;
55
56    private ViewGroup mRootView;
57
58    private LayoutInflater mInflater;
59
60    private CacheData mCacheData;
61
62    // A map for all address fields except for country.
63    private final EnumMap<AddressField, AddressUiComponent> mInputWidgets =
64        new EnumMap<AddressField, AddressUiComponent>(AddressField.class);
65
66    private FormController mFormController;
67
68    private FormatInterpreter mFormatInterpreter;
69
70    private FormOptions mFormOptions;
71
72    private StandardAddressVerifier mVerifier;
73
74    private ProgressDialog mProgressDialog;
75
76    private String mCurrentRegion;
77
78    // The current language the widget use in BCP47 format. It differs from the default locale of
79    // the phone in that it contains information on the script to use.
80    private String mWidgetLocale;
81
82    private ScriptType mScript;
83
84    // The appropriate label that should be applied to the locality (city) field of the current
85    // country.  Examples include "city" or "district".
86    private String mLocalityLabel;
87
88    // The appropriate label that should be applied to the admin area field of the current country.
89    // Examples include "state", "province", "emirate", etc.
90    private String mAdminLabel;
91
92    private static final Map<String, Integer> ADMIN_LABELS;
93    private static final Map<String, Integer> LOCALITY_LABELS;
94    private static final Map<String, Integer> ADMIN_ERROR_MESSAGES;
95
96    private static final FormOptions SHOW_ALL_FIELDS = new FormOptions.Builder().build();
97
98    // The appropriate label that should be applied to the zip code field of the current country.
99    private enum ZipLabel {
100        ZIP,
101        POSTAL
102    }
103
104    private ZipLabel mZipLabel;
105
106    static {
107        Map<String, Integer> adminLabelMap = new HashMap<String, Integer>(15);
108        adminLabelMap.put("area", R.string.i18n_area);
109        adminLabelMap.put("county", R.string.i18n_county_label);
110        adminLabelMap.put("department", R.string.i18n_department);
111        adminLabelMap.put("district", R.string.i18n_dependent_locality_label);
112        adminLabelMap.put("do_si", R.string.i18n_do_si);
113        adminLabelMap.put("emirate", R.string.i18n_emirate);
114        adminLabelMap.put("island", R.string.i18n_island);
115        adminLabelMap.put("oblast", R.string.i18n_oblast);
116        adminLabelMap.put("parish", R.string.i18n_parish);
117        adminLabelMap.put("prefecture", R.string.i18n_prefecture);
118        adminLabelMap.put("province", R.string.i18n_province);
119        adminLabelMap.put("state", R.string.i18n_state_label);
120        ADMIN_LABELS = Collections.unmodifiableMap(adminLabelMap);
121
122        Map<String, Integer> localityLabelMap = new HashMap<String, Integer>(2);
123        localityLabelMap.put("city", R.string.i18n_locality_label);
124        localityLabelMap.put("district", R.string.i18n_dependent_locality_label);
125        LOCALITY_LABELS = Collections.unmodifiableMap(localityLabelMap);
126
127        Map<String, Integer> adminErrorMap = new HashMap<String, Integer>(15);
128        adminErrorMap.put("area", R.string.invalid_area);
129        adminErrorMap.put("county", R.string.invalid_county_label);
130        adminErrorMap.put("department", R.string.invalid_department);
131        adminErrorMap.put("district", R.string.invalid_dependent_locality_label);
132        adminErrorMap.put("do_si", R.string.invalid_do_si);
133        adminErrorMap.put("emirate", R.string.invalid_emirate);
134        adminErrorMap.put("island", R.string.invalid_island);
135        adminErrorMap.put("oblast", R.string.invalid_oblast);
136        adminErrorMap.put("parish", R.string.invalid_parish);
137        adminErrorMap.put("prefecture", R.string.invalid_prefecture);
138        adminErrorMap.put("province", R.string.invalid_province);
139        adminErrorMap.put("state", R.string.invalid_state_label);
140        ADMIN_ERROR_MESSAGES = Collections.unmodifiableMap(adminErrorMap);
141    }
142
143    // Need handler for callbacks to the UI thread
144    final Handler mHandler = new Handler();
145
146    final Runnable mUpdateMultipleFields = new Runnable() {
147        @Override
148        public void run() {
149            updateFields();
150        }
151    };
152
153    private class UpdateRunnable implements Runnable {
154        private AddressField myId;
155
156        public UpdateRunnable(AddressField id) {
157            myId = id;
158        }
159
160        @Override
161        public void run() {
162            updateInputWidget(myId);
163        }
164    }
165
166    private static class AddressSpinnerInfo {
167        private Spinner mView;
168
169        private AddressField mId;
170
171        private AddressField mParentId;
172
173        private ArrayAdapter<String> mAdapter;
174
175        private List<RegionData> mCurrentRegions;
176
177        @SuppressWarnings("unchecked")
178        public AddressSpinnerInfo(Spinner view, AddressField id, AddressField parentId) {
179            mView = view;
180            mId = id;
181            mParentId = parentId;
182            mAdapter = (ArrayAdapter<String>) view.getAdapter();
183        }
184
185        public void setSpinnerList(List<RegionData> list, String defaultKey) {
186            mCurrentRegions = list;
187            mAdapter.clear();
188            for (RegionData item : list) {
189                mAdapter.add(item.getDisplayName());
190            }
191            mAdapter.sort(Collator.getInstance(Locale.getDefault()));
192            if (defaultKey.length() == 0) {
193                mView.setSelection(0);
194            } else {
195                int position = mAdapter.getPosition(defaultKey);
196                mView.setSelection(position);
197            }
198        }
199
200        // Returns the region key of the currently selected region in the Spinner.
201        public String getRegionCode(int position) {
202            if (mAdapter.getCount() <= position) {
203                return "";
204            }
205            String value = mAdapter.getItem(position);
206            return getRegionDataKeyForValue(value);
207        }
208
209        // Returns the region key for the region value.
210        public String getRegionDataKeyForValue(String value) {
211            for (RegionData data : mCurrentRegions) {
212                if (data.getDisplayName().endsWith(value)) {
213                    return data.getKey();
214                }
215            }
216            return "";
217        }
218    }
219
220    private final ArrayList<AddressSpinnerInfo> mSpinners = new ArrayList<AddressSpinnerInfo>();
221
222    private AddressWidgetUiComponentProvider mComponentProvider;
223
224    /** TODO: Add region-dependent width types for address fields. */
225    private WidthType getFieldWidthType(AddressUiComponent field) {
226        return field.getId().getDefaulWidthType();
227    }
228
229    private void createView(ViewGroup rootView, AddressUiComponent field, String defaultKey,
230            boolean readOnly) {
231        @SuppressWarnings("deprecation")  // FILL_PARENT renamed MATCH_PARENT in API Level 8.
232        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT,
233                LayoutParams.WRAP_CONTENT);
234        String fieldText = field.getFieldName();
235        WidthType widthType = getFieldWidthType(field);
236
237        if (fieldText.length() > 0) {
238            TextView textView = mComponentProvider.createUiLabel(fieldText, widthType);
239            rootView.addView(textView, lp);
240        }
241        if (field.getUiType().equals(UiComponent.EDIT)) {
242            EditText editText = mComponentProvider.createUiTextField(widthType);
243            field.setView(editText);
244            editText.setEnabled(!readOnly);
245            rootView.addView(editText, lp);
246        } else if (field.getUiType().equals(UiComponent.SPINNER)) {
247            ArrayAdapter<String> adapter = mComponentProvider.createUiPickerAdapter(widthType);
248            Spinner spinner = mComponentProvider.createUiPickerSpinner(widthType);
249
250            field.setView(spinner);
251            rootView.addView(spinner, lp);
252            spinner.setAdapter(adapter);
253            AddressSpinnerInfo spinnerInfo =
254                    new AddressSpinnerInfo(spinner, field.getId(), field.getParentId());
255            spinnerInfo.setSpinnerList(field.getCandidatesList(), defaultKey);
256
257            if (fieldText.length() > 0) {
258                spinner.setPrompt(fieldText);
259            }
260            spinner.setOnItemSelectedListener(this);
261            mSpinners.add(spinnerInfo);
262        }
263    }
264
265    /**
266     *  Associates each field with its corresponding AddressUiComponent.
267     */
268    private void buildFieldWidgets() {
269        AddressData data = new AddressData.Builder().setCountry(mCurrentRegion).build();
270        LookupKey key = new LookupKey.Builder(LookupKey.KeyType.DATA).setAddressData(data).build();
271        AddressVerificationNodeData countryNode =
272            (new ClientData(mCacheData)).getDefaultData(key.toString());
273
274        // Set up AddressField.ADMIN_AREA
275        AddressUiComponent adminAreaUi = new AddressUiComponent(AddressField.ADMIN_AREA);
276        adminAreaUi.setFieldName(getAdminAreaFieldName(countryNode));
277        mInputWidgets.put(AddressField.ADMIN_AREA, adminAreaUi);
278
279        // Set up AddressField.LOCALITY
280        AddressUiComponent localityUi = new AddressUiComponent(AddressField.LOCALITY);
281        localityUi.setFieldName(getLocalityFieldName(countryNode));
282        mInputWidgets.put(AddressField.LOCALITY, localityUi);
283
284        // Set up AddressField.DEPENDENT_LOCALITY
285        AddressUiComponent subLocalityUi = new AddressUiComponent(AddressField.DEPENDENT_LOCALITY);
286        subLocalityUi.setFieldName(mContext.getString(R.string.i18n_dependent_locality_label));
287        mInputWidgets.put(AddressField.DEPENDENT_LOCALITY, subLocalityUi);
288
289        // Set up AddressField.ADDRESS_LINE_1
290        AddressUiComponent addressLine1Ui = new AddressUiComponent(AddressField.ADDRESS_LINE_1);
291        addressLine1Ui.setFieldName(mContext.getString(R.string.i18n_address_line1_label));
292        mInputWidgets.put(AddressField.ADDRESS_LINE_1, addressLine1Ui);
293
294        // Set up AddressField.ADDRESS_LINE_2
295        AddressUiComponent addressLine2Ui = new AddressUiComponent(AddressField.ADDRESS_LINE_2);
296        addressLine2Ui.setFieldName("");
297        mInputWidgets.put(AddressField.ADDRESS_LINE_2, addressLine2Ui);
298
299        // Set up AddressField.ORGANIZATION
300        AddressUiComponent organizationUi = new AddressUiComponent(AddressField.ORGANIZATION);
301        organizationUi.setFieldName(mContext.getString(R.string.i18n_organization_label));
302        mInputWidgets.put(AddressField.ORGANIZATION, organizationUi);
303
304        // Set up AddressField.RECIPIENT
305        AddressUiComponent recipientUi = new AddressUiComponent(AddressField.RECIPIENT);
306        recipientUi.setFieldName(mContext.getString(R.string.i18n_recipient_label));
307        mInputWidgets.put(AddressField.RECIPIENT, recipientUi);
308
309        // Set up AddressField.POSTAL_CODE
310        AddressUiComponent postalCodeUi = new AddressUiComponent(AddressField.POSTAL_CODE);
311        postalCodeUi.setFieldName(getZipFieldName(countryNode));
312        mInputWidgets.put(AddressField.POSTAL_CODE, postalCodeUi);
313
314        // Set up AddressField.SORTING_CODE
315        AddressUiComponent sortingCodeUi = new AddressUiComponent(AddressField.SORTING_CODE);
316        sortingCodeUi.setFieldName("CEDEX");
317        mInputWidgets.put(AddressField.SORTING_CODE, sortingCodeUi);
318    }
319
320    private void initializeDropDowns() {
321        AddressUiComponent adminAreaUi = mInputWidgets.get(AddressField.ADMIN_AREA);
322        List<RegionData> adminAreaList = getRegionData(AddressField.COUNTRY);
323        adminAreaUi.initializeCandidatesList(adminAreaList);
324
325        AddressUiComponent localityUi = mInputWidgets.get(AddressField.LOCALITY);
326        List<RegionData> localityList = getRegionData(AddressField.ADMIN_AREA);
327        localityUi.initializeCandidatesList(localityList);
328    }
329
330    // Zip code is called postal code in some countries. This method returns the appropriate name
331    // for the given countryNode.
332    private String getZipFieldName(AddressVerificationNodeData countryNode) {
333        String zipName;
334        String zipType = countryNode.get(AddressDataKey.ZIP_NAME_TYPE);
335        if (zipType == null) {
336            mZipLabel = ZipLabel.POSTAL;
337            zipName = mContext.getString(R.string.i18n_postal_code_label);
338        } else {
339            mZipLabel = ZipLabel.ZIP;
340            zipName = mContext.getString(R.string.i18n_zip_code_label);
341        }
342        return zipName;
343    }
344
345    private String getLocalityFieldName(AddressVerificationNodeData countryNode) {
346        String localityLabelType = countryNode.get(AddressDataKey.LOCALITY_NAME_TYPE);
347        mLocalityLabel = localityLabelType;
348        Integer result = LOCALITY_LABELS.get(localityLabelType);
349        if (result == null) {
350            // Fallback to city.
351            result = R.string.i18n_locality_label;
352        }
353        return mContext.getString(result);
354    }
355
356    private String getAdminAreaFieldName(AddressVerificationNodeData countryNode) {
357        String adminLabelType = countryNode.get(AddressDataKey.STATE_NAME_TYPE);
358        mAdminLabel = adminLabelType;
359        Integer result = ADMIN_LABELS.get(adminLabelType);
360        if (result == null) {
361            // Fallback to province.
362            result = R.string.i18n_province;
363        }
364        return mContext.getString(result);
365    }
366
367    private void buildCountryListBox() {
368        // Set up AddressField.COUNTRY
369        AddressUiComponent countryUi = new AddressUiComponent(AddressField.COUNTRY);
370        countryUi.setFieldName(mContext.getString(R.string.i18n_country_label));
371        ArrayList<RegionData> countries = new ArrayList<RegionData>();
372        for (RegionData regionData : mFormController.getRegionData(new LookupKey.Builder(
373                KeyType.DATA).build())) {
374            String regionKey = regionData.getKey();
375            // ZZ represents an unknown region code.
376            if (!regionKey.equals("ZZ")) {
377                String localCountryName = getLocalCountryName(regionKey);
378                RegionData country = new RegionData.Builder().setKey(regionKey).setName(
379                        localCountryName).build();
380                countries.add(country);
381            }
382        }
383        countryUi.initializeCandidatesList(countries);
384        mInputWidgets.put(AddressField.COUNTRY, countryUi);
385    }
386
387    private String getLocalCountryName(String regionCode) {
388        return (new Locale("", regionCode)).getDisplayCountry(Locale.getDefault());
389    }
390
391    private AddressSpinnerInfo findSpinnerByView(View view) {
392        for (AddressSpinnerInfo spinnerInfo : mSpinners) {
393            if (spinnerInfo.mView == view) {
394                return spinnerInfo;
395            }
396        }
397        return null;
398    }
399
400    private void updateFields() {
401        removePreviousViews();
402        buildFieldWidgets();
403        initializeDropDowns();
404        layoutAddressFields();
405    }
406
407    private void removePreviousViews() {
408        if (mRootView == null) {
409            return;
410        }
411        int childCount = mRootView.getChildCount();
412        if (mFormOptions.isHidden(AddressField.COUNTRY)) {
413            if (childCount > 0) {
414                mRootView.removeAllViews();
415            }
416        } else if (childCount > 2) {
417            // Keep the TextView and Spinner for Country and remove everything else.
418            mRootView.removeViews(2, mRootView.getChildCount() - 2);
419        }
420    }
421
422    private void layoutAddressFields() {
423        for (AddressField field : mFormatInterpreter.getAddressFieldOrder(mScript,
424                mCurrentRegion)) {
425            if (!mFormOptions.isHidden(field)) {
426              createView(mRootView, mInputWidgets.get(field), "", mFormOptions.isReadonly(field));
427            }
428        }
429    }
430
431    private void updateChildNodes(AdapterView<?> parent, int position) {
432        AddressSpinnerInfo spinnerInfo = findSpinnerByView(parent);
433        if (spinnerInfo == null) {
434            return;
435        }
436
437        // Find all the child spinners, if any, that depend on this one.
438        final AddressField myId = spinnerInfo.mId;
439        if (myId != AddressField.COUNTRY && myId != AddressField.ADMIN_AREA
440                && myId != AddressField.LOCALITY) {
441            // Only a change in the three AddressFields above will trigger a change in other
442            // AddressFields. Therefore, for all other AddressFields, we return immediately.
443            return;
444        }
445
446        String regionCode = spinnerInfo.getRegionCode(position);
447        if (myId == AddressField.COUNTRY) {
448            updateWidgetOnCountryChange(regionCode);
449            return;
450        }
451
452        mFormController.requestDataForAddress(getAddressData(), new DataLoadListener() {
453            @Override
454            public void dataLoadingBegin(){
455            }
456
457            @Override
458            public void dataLoadingEnd() {
459                Runnable updateChild = new UpdateRunnable(myId);
460                mHandler.post(updateChild);
461            }
462        });
463    }
464
465    public void updateWidgetOnCountryChange(String regionCode) {
466        if (mCurrentRegion.equalsIgnoreCase(regionCode)) {
467            return;
468        }
469        mCurrentRegion = regionCode;
470        mFormController.setCurrentCountry(mCurrentRegion);
471        renderForm();
472    }
473
474    private void updateInputWidget(AddressField myId) {
475        for (AddressSpinnerInfo child : mSpinners) {
476            if (child.mParentId == myId) {
477                List<RegionData> candidates = getRegionData(child.mParentId);
478                child.setSpinnerList(candidates, "");
479            }
480        }
481    }
482
483    public void renderForm() {
484        setWidgetLocaleAndScript();
485        AddressData data = new AddressData.Builder().setCountry(mCurrentRegion)
486                .setLanguageCode(mWidgetLocale).build();
487        mFormController.requestDataForAddress(data, new DataLoadListener() {
488            @Override
489            public void dataLoadingBegin() {
490                mProgressDialog = mComponentProvider.getUiActivityIndicatorView();
491                mProgressDialog.setMessage(mContext.getString(R.string.address_data_loading));
492                Log.d(this.toString(), "Progress dialog started.");
493            }
494            @Override
495            public void dataLoadingEnd() {
496                Log.d(this.toString(), "Data loading completed.");
497                mProgressDialog.dismiss();
498                Log.d(this.toString(), "Progress dialog stopped.");
499                mHandler.post(mUpdateMultipleFields);
500            }
501        });
502    }
503
504    private void setWidgetLocaleAndScript() {
505        mWidgetLocale = Util.getWidgetCompatibleLanguageCode(Locale.getDefault(), mCurrentRegion);
506        mFormController.setLanguageCode(mWidgetLocale);
507        mScript = Util.isExplicitLatinScript(mWidgetLocale)
508                ? ScriptType.LATIN
509                : ScriptType.LOCAL;
510    }
511
512    private List<RegionData> getRegionData(AddressField parentField) {
513        AddressData address = getAddressData();
514
515        // Removes language code from address if it is default. This address is used to build
516        // lookup key, which neglects default language. For example, instead of "data/US--en/CA",
517        // the right lookup key is "data/US/CA".
518        if (mFormController.isDefaultLanguage(address.getLanguageCode())) {
519            address = new AddressData.Builder(address).setLanguageCode(null).build();
520        }
521
522        LookupKey parentKey = mFormController.getDataKeyFor(address).getKeyForUpperLevelField(
523                parentField);
524        List<RegionData> candidates;
525        // Can't build a key with parent field, quit.
526        if (parentKey == null) {
527            Log.w(this.toString(), "Can't build key with parent field " + parentField + ". One of"
528                    + " the ancestor fields might be empty");
529
530            // Removes candidates that exist from previous settings. For example, data/US has a
531            // list of candidates AB, BC, CA, etc, that list should be cleaned up when user updates
532            // the address by changing country to Channel Islands.
533            candidates = new ArrayList<RegionData>(1);
534        } else {
535            candidates = mFormController.getRegionData(parentKey);
536        }
537        return candidates;
538    }
539
540    /**
541     * Creates an AddressWidget to be attached to rootView for the specific context using the
542     * default UI component provider.
543     */
544    public AddressWidget(Context context, ViewGroup rootView, FormOptions formOptions,
545            ClientCacheManager cacheManager) {
546        this(context, rootView, formOptions, cacheManager,
547                new AddressWidgetUiComponentProvider(context));
548    }
549
550    /**
551     * Creates an AddressWidget to be attached to rootView for the specific context using UI
552     * component provided by the provider.
553     */
554    public AddressWidget(Context context, ViewGroup rootView, FormOptions formOptions,
555            ClientCacheManager cacheManager, AddressWidgetUiComponentProvider provider) {
556        mComponentProvider = provider;
557        mCurrentRegion =
558            ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE))
559                    .getSimCountryIso().toUpperCase(Locale.US);
560        if (mCurrentRegion.length() == 0) {
561            mCurrentRegion = "US";
562        }
563        init(context, rootView, formOptions, cacheManager);
564        renderForm();
565    }
566
567    /**
568     * Creates an AddressWidget to be attached to rootView for the specific context using the
569     * default UI component provider, and fill out the address form with savedAddress.
570     */
571    public AddressWidget(Context context, ViewGroup rootView, FormOptions formOptions,
572            ClientCacheManager cacheManager, AddressData savedAddress) {
573        this(context, rootView, formOptions, cacheManager, savedAddress,
574                new AddressWidgetUiComponentProvider(context));
575    }
576
577    /**
578     * Creates an AddressWidget to be attached to rootView for the specific context using UI
579     * component provided by the provider, and fill out the address form with savedAddress.
580     */
581    public AddressWidget(Context context, ViewGroup rootView, FormOptions formOptions,
582            ClientCacheManager cacheManager, AddressData savedAddress,
583            AddressWidgetUiComponentProvider provider) {
584        mComponentProvider = provider;
585        mCurrentRegion = savedAddress.getPostalCountry();
586        // Postal country must be 2 letter country code. Otherwise default to US.
587        if (mCurrentRegion == null || mCurrentRegion.length() != 2) {
588            mCurrentRegion = "US";
589        }
590        init(context, rootView, formOptions, cacheManager);
591        renderFormWithSavedAddress(savedAddress);
592    }
593
594    public void renderFormWithSavedAddress(AddressData savedAddress) {
595        setWidgetLocaleAndScript();
596        removePreviousViews();
597        buildFieldWidgets();
598        layoutAddressFields();
599        initializeFieldsWithAddress(savedAddress);
600    }
601
602    private void initializeFieldsWithAddress(AddressData savedAddress) {
603        for (AddressField field : mFormatInterpreter.getAddressFieldOrder(mScript,
604                mCurrentRegion)) {
605            String value = savedAddress.getFieldValue(field);
606            if (value == null) {
607                value = "";
608            }
609            AddressUiComponent uiComponent = mInputWidgets.get(field);
610            EditText view = (EditText) uiComponent.getView();
611            if (view != null) {
612               view.setText(value);
613            }
614        }
615    }
616
617    private void init(Context context, ViewGroup rootView, FormOptions formOptions,
618            ClientCacheManager cacheManager) {
619        mContext = context;
620        mRootView = rootView;
621        mFormOptions = formOptions;
622        mCacheData = new CacheData(cacheManager);
623        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
624        mFormController =
625            new FormController(new ClientData(mCacheData),
626                               mWidgetLocale, mCurrentRegion);
627        mFormatInterpreter = new FormatInterpreter(mFormOptions);
628        mVerifier = new StandardAddressVerifier(
629                new FieldVerifier(new ClientData(mCacheData)));
630        if (!formOptions.isHidden(AddressField.COUNTRY)) {
631            buildCountryListBox();
632            createView(mRootView, mInputWidgets.get(AddressField.COUNTRY),
633                    getLocalCountryName(mCurrentRegion),
634                    formOptions.isReadonly(AddressField.COUNTRY));
635        }
636    }
637
638    /**
639     * Sets address data server URL. Input URL cannot be null.
640     *
641     * @param url The service URL.
642     */
643    public void setUrl(String url) {
644        mCacheData.setUrl(url);
645    }
646
647    /**
648     * Gets user input address in AddressData format.
649     */
650    public AddressData getAddressData() {
651        AddressData.Builder builder = new AddressData.Builder();
652        builder.setCountry(mCurrentRegion);
653        for (AddressField field : mFormatInterpreter.getAddressFieldOrder(mScript,
654                mCurrentRegion)) {
655            AddressUiComponent addressUiComponent = mInputWidgets.get(field);
656            if (addressUiComponent != null) {
657                String value = addressUiComponent.getValue();
658                if (addressUiComponent.getUiType() == UiComponent.SPINNER) {
659                     // For drop-downs, return the key of the region selected instead of the value.
660                     View view = getViewForField(field);
661                     AddressSpinnerInfo spinnerInfo = findSpinnerByView(view);
662                     if (spinnerInfo != null) {
663                         value = spinnerInfo.getRegionDataKeyForValue(value);
664                     }
665                }
666                builder.set(field, value);
667            }
668        }
669        builder.setLanguageCode(mWidgetLocale);
670        return builder.build();
671    }
672
673    /**
674     * Gets the formatted address.
675     *
676     * This method does not validate addresses. Also, it will "normalize" the result strings by
677     * removing redundant spaces and empty lines.
678     *
679     * @return the formatted address
680     */
681    public List<String> getEnvelopeAddress() {
682        return mFormatInterpreter.getEnvelopeAddress(getAddressData());
683    }
684
685    /**
686     * Gets the formatted address based on the AddressData passed in.
687     */
688    public List<String> getEnvelopeAddress(AddressData address) {
689        return mFormatInterpreter.getEnvelopeAddress(address);
690    }
691
692    /**
693     * Gets the formatted address based on the AddressData passed in with none of the relevant
694     * fields hidden.
695     */
696    public static List<String> getFullEnvelopeAddress(AddressData address) {
697        return new FormatInterpreter(SHOW_ALL_FIELDS).getEnvelopeAddress(address);
698    }
699
700    /**
701     * Get problems found in the address data entered by the user.
702     */
703    public AddressProblems getAddressProblems() {
704        AddressProblems problems = new AddressProblems();
705        AddressData addressData = getAddressData();
706        mVerifier.verify(addressData, problems);
707        return problems;
708    }
709
710    /**
711     * Displays an appropriate error message when the AddressField contains an invalid entry.
712     *
713     * @return the View object representing the AddressField.
714     */
715    public View displayErrorMessageForInvalidEntryIn(AddressField field) {
716        Log.d(this.toString(), "Display error message for the field: " + field.toString());
717        AddressUiComponent addressUiComponent = mInputWidgets.get(field);
718        if (addressUiComponent != null && addressUiComponent.getUiType() == UiComponent.EDIT) {
719            int errorMessageId = getErrorMessageIdForInvalidEntryIn(field);
720            EditText view = (EditText) addressUiComponent.getView();
721            view.setError(mContext.getString(errorMessageId));
722            return view;
723        }
724        return null;
725    }
726
727    private int getErrorMessageIdForInvalidEntryIn(AddressField field) {
728        switch (field) {
729            case ADMIN_AREA:
730                return ADMIN_ERROR_MESSAGES.get(mAdminLabel);
731            case LOCALITY:
732                return R.string.invalid_locality_label;
733            case DEPENDENT_LOCALITY:
734                return R.string.invalid_dependent_locality_label;
735            case POSTAL_CODE:
736                return (mZipLabel == ZipLabel.POSTAL
737                        ? R.string.invalid_postal_code_label
738                        : R.string.invalid_zip_code_label);
739            default:
740                return R.string.invalid_entry;
741        }
742    }
743
744    /**
745     * Clears all error messages in the UI.
746     */
747    public void clearErrorMessage() {
748        for (AddressField field : mFormatInterpreter.getAddressFieldOrder(mScript,
749                mCurrentRegion)) {
750            AddressUiComponent addressUiComponent = mInputWidgets.get(field);
751
752            if (addressUiComponent != null && addressUiComponent.getUiType() == UiComponent.EDIT) {
753                EditText view = (EditText) addressUiComponent.getView();
754                if (view != null) {
755                    view.setError(null);
756                }
757            }
758        }
759    }
760
761    public View getViewForField(AddressField field) {
762      AddressUiComponent component = mInputWidgets.get(field);
763      if (component == null) {
764        return null;
765      }
766      return component.getView();
767    }
768
769    @Override
770    public void onNothingSelected(AdapterView<?> arg0) {
771    }
772
773    @Override
774    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
775        updateChildNodes(parent, position);
776    }
777}
778