1/*
2 * Copyright (C) 2009 The Android Open Source Project
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.contacts.common.model.account;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.res.Resources;
22import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
23import android.provider.ContactsContract.CommonDataKinds.Email;
24import android.provider.ContactsContract.CommonDataKinds.Event;
25import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
26import android.provider.ContactsContract.CommonDataKinds.Im;
27import android.provider.ContactsContract.CommonDataKinds.Nickname;
28import android.provider.ContactsContract.CommonDataKinds.Note;
29import android.provider.ContactsContract.CommonDataKinds.Organization;
30import android.provider.ContactsContract.CommonDataKinds.Phone;
31import android.provider.ContactsContract.CommonDataKinds.Photo;
32import android.provider.ContactsContract.CommonDataKinds.Relation;
33import android.provider.ContactsContract.CommonDataKinds.SipAddress;
34import android.provider.ContactsContract.CommonDataKinds.StructuredName;
35import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
36import android.provider.ContactsContract.CommonDataKinds.Website;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.view.inputmethod.EditorInfo;
40
41import com.android.contacts.common.R;
42import com.android.contacts.common.model.dataitem.DataKind;
43import com.android.contacts.common.test.NeededForTesting;
44import com.android.contacts.common.util.CommonDateUtils;
45import com.android.contacts.common.util.ContactDisplayUtils;
46import com.google.common.collect.Lists;
47import com.google.common.collect.Maps;
48
49import org.xmlpull.v1.XmlPullParser;
50import org.xmlpull.v1.XmlPullParserException;
51
52import java.io.IOException;
53import java.util.List;
54import java.util.Locale;
55import java.util.Map;
56
57public abstract class BaseAccountType extends AccountType {
58    private static final String TAG = "BaseAccountType";
59
60    protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
61    protected static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
62            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
63    protected static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT
64            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
65    protected static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT
66            | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
67    protected static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT
68            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
69    protected static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT
70            | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
71    protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
72    protected static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT
73            | EditorInfo.TYPE_TEXT_VARIATION_URI;
74    protected static final int FLAGS_POSTAL = EditorInfo.TYPE_CLASS_TEXT
75            | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
76            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
77    protected static final int FLAGS_SIP_ADDRESS = EditorInfo.TYPE_CLASS_TEXT
78            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;  // since SIP addresses have the same
79                                                             // basic format as email addresses
80    protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
81            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
82
83    // Specify the maximum number of lines that can be used to display various field types.  If no
84    // value is specified for a particular type, we use the default value from {@link DataKind}.
85    protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
86    protected static final int MAX_LINES_FOR_GROUP = 10;
87    protected static final int MAX_LINES_FOR_NOTE = 100;
88
89    private interface Tag {
90        static final String DATA_KIND = "DataKind";
91        static final String TYPE = "Type";
92    }
93
94    private interface Attr {
95        static final String MAX_OCCURRENCE = "maxOccurs";
96        static final String DATE_WITH_TIME = "dateWithTime";
97        static final String YEAR_OPTIONAL = "yearOptional";
98        static final String KIND = "kind";
99        static final String TYPE = "type";
100    }
101
102    private interface Weight {
103        static final int NONE = -1;
104        static final int ORGANIZATION = 5;
105        static final int PHONE = 10;
106        static final int EMAIL = 15;
107        static final int IM = 20;
108        static final int STRUCTURED_POSTAL = 25;
109        static final int NOTE = 110;
110        static final int NICKNAME = 115;
111        static final int WEBSITE = 120;
112        static final int SIP_ADDRESS = 130;
113        static final int EVENT = 150;
114        static final int RELATIONSHIP = 160;
115        static final int GROUP_MEMBERSHIP = 999;
116    }
117
118    public BaseAccountType() {
119        this.accountType = null;
120        this.dataSet = null;
121        this.titleRes = R.string.account_phone;
122        this.iconRes = R.mipmap.ic_launcher_contacts;
123    }
124
125    protected static EditType buildPhoneType(int type) {
126        return new EditType(type, Phone.getTypeLabelResource(type));
127    }
128
129    protected static EditType buildEmailType(int type) {
130        return new EditType(type, Email.getTypeLabelResource(type));
131    }
132
133    protected static EditType buildPostalType(int type) {
134        return new EditType(type, StructuredPostal.getTypeLabelResource(type));
135    }
136
137    protected static EditType buildImType(int type) {
138        return new EditType(type, Im.getProtocolLabelResource(type));
139    }
140
141    protected static EditType buildEventType(int type, boolean yearOptional) {
142        return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
143    }
144
145    protected static EditType buildRelationType(int type) {
146        return new EditType(type, Relation.getTypeLabelResource(type));
147    }
148
149    protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
150        DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
151                R.string.nameLabelsGroup, -1, true));
152        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
153        kind.actionBody = new SimpleInflater(Nickname.NAME);
154        kind.typeOverallMax = 1;
155
156        kind.fieldList = Lists.newArrayList();
157        kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
158                R.string.full_name, FLAGS_PERSON_NAME));
159        kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
160                FLAGS_PERSON_NAME).setLongForm(true));
161        kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
162                FLAGS_PERSON_NAME).setLongForm(true));
163        kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
164                FLAGS_PERSON_NAME).setLongForm(true));
165        kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
166                FLAGS_PERSON_NAME).setLongForm(true));
167        kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
168                FLAGS_PERSON_NAME).setLongForm(true));
169        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
170                R.string.name_phonetic_family, FLAGS_PHONETIC));
171        kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
172                R.string.name_phonetic_middle, FLAGS_PHONETIC));
173        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
174                R.string.name_phonetic_given, FLAGS_PHONETIC));
175
176        return kind;
177    }
178
179    protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
180        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
181                R.string.nameLabelsGroup, -1, true));
182        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
183        kind.actionBody = new SimpleInflater(Nickname.NAME);
184        kind.typeOverallMax = 1;
185
186        kind.fieldList = Lists.newArrayList();
187        kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
188                R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
189
190        boolean displayOrderPrimary =
191                context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
192
193        if (!displayOrderPrimary) {
194            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
195                    FLAGS_PERSON_NAME).setLongForm(true));
196            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
197                    FLAGS_PERSON_NAME).setLongForm(true));
198            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
199                    FLAGS_PERSON_NAME).setLongForm(true));
200            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
201                    FLAGS_PERSON_NAME).setLongForm(true));
202            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
203                    FLAGS_PERSON_NAME).setLongForm(true));
204        } else {
205            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
206                    FLAGS_PERSON_NAME).setLongForm(true));
207            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
208                    FLAGS_PERSON_NAME).setLongForm(true));
209            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
210                    FLAGS_PERSON_NAME).setLongForm(true));
211            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
212                    FLAGS_PERSON_NAME).setLongForm(true));
213            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
214                    FLAGS_PERSON_NAME).setLongForm(true));
215        }
216
217        return kind;
218    }
219
220    protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
221        DataKind kind = addKind(new DataKind(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
222                R.string.name_phonetic, -1, true));
223        kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
224        kind.actionBody = new SimpleInflater(Nickname.NAME);
225        kind.typeOverallMax = 1;
226
227        kind.fieldList = Lists.newArrayList();
228        kind.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
229                R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
230        kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
231                R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
232        kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
233                R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
234        kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
235                R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
236
237        return kind;
238    }
239
240    protected DataKind addDataKindNickname(Context context) throws DefinitionException {
241        DataKind kind = addKind(new DataKind(Nickname.CONTENT_ITEM_TYPE,
242                    R.string.nicknameLabelsGroup, 115, true));
243        kind.typeOverallMax = 1;
244        kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
245        kind.actionBody = new SimpleInflater(Nickname.NAME);
246        kind.defaultValues = new ContentValues();
247        kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
248
249        kind.fieldList = Lists.newArrayList();
250        kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
251                FLAGS_PERSON_NAME));
252
253        return kind;
254    }
255
256    protected DataKind addDataKindPhone(Context context) throws DefinitionException {
257        DataKind kind = addKind(new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup,
258                10, true));
259        kind.iconAltRes = R.drawable.ic_text_holo_light;
260        kind.iconAltDescriptionRes = R.string.sms;
261        kind.actionHeader = new PhoneActionInflater();
262        kind.actionAltHeader = new PhoneActionAltInflater();
263        kind.actionBody = new SimpleInflater(Phone.NUMBER);
264        kind.typeColumn = Phone.TYPE;
265        kind.typeList = Lists.newArrayList();
266        kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
267        kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
268        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
269        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
270        kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
271        kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
272        kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
273        kind.typeList.add(
274                buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
275        kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
276        kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
277        kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
278        kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
279        kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
280        kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
281        kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
282        kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
283        kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
284        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
285        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
286        kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
287        kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
288
289        kind.fieldList = Lists.newArrayList();
290        kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
291
292        return kind;
293    }
294
295    protected DataKind addDataKindEmail(Context context) throws DefinitionException {
296        DataKind kind = addKind(new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup,
297                15, true));
298        kind.actionHeader = new EmailActionInflater();
299        kind.actionBody = new SimpleInflater(Email.DATA);
300        kind.typeColumn = Email.TYPE;
301        kind.typeList = Lists.newArrayList();
302        kind.typeList.add(buildEmailType(Email.TYPE_HOME));
303        kind.typeList.add(buildEmailType(Email.TYPE_WORK));
304        kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
305        kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
306        kind.typeList.add(
307                buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
308
309        kind.fieldList = Lists.newArrayList();
310        kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
311
312        return kind;
313    }
314
315    protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
316        DataKind kind = addKind(new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
317                R.string.postalLabelsGroup, 25, true));
318        kind.actionHeader = new PostalActionInflater();
319        kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
320        kind.typeColumn = StructuredPostal.TYPE;
321        kind.typeList = Lists.newArrayList();
322        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
323        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
324        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
325        kind.typeList.add(buildPostalType(StructuredPostal.TYPE_CUSTOM).setSecondary(true)
326                .setCustomColumn(StructuredPostal.LABEL));
327
328        kind.fieldList = Lists.newArrayList();
329        kind.fieldList.add(
330                new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
331                        FLAGS_POSTAL));
332
333        kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
334
335        return kind;
336    }
337
338    protected DataKind addDataKindIm(Context context) throws DefinitionException {
339        DataKind kind = addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, 20,
340                true));
341        kind.actionHeader = new ImActionInflater();
342        kind.actionBody = new SimpleInflater(Im.DATA);
343
344        // NOTE: even though a traditional "type" exists, for editing
345        // purposes we're using the protocol to pick labels
346
347        kind.defaultValues = new ContentValues();
348        kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
349
350        kind.typeColumn = Im.PROTOCOL;
351        kind.typeList = Lists.newArrayList();
352        kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
353        kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
354        kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
355        kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
356        kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
357        kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
358        kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
359        kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
360        kind.typeList.add(buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(
361                Im.CUSTOM_PROTOCOL));
362
363        kind.fieldList = Lists.newArrayList();
364        kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
365
366        return kind;
367    }
368
369    protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
370        DataKind kind = addKind(new DataKind(Organization.CONTENT_ITEM_TYPE,
371                    R.string.organizationLabelsGroup, 5, true));
372        kind.actionHeader = new SimpleInflater(Organization.COMPANY);
373        kind.actionBody = new SimpleInflater(Organization.TITLE);
374        kind.typeOverallMax = 1;
375
376        kind.fieldList = Lists.newArrayList();
377        kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
378                FLAGS_GENERIC_NAME));
379        kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
380                FLAGS_GENERIC_NAME));
381
382        return kind;
383    }
384
385    protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
386        DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, true));
387        kind.typeOverallMax = 1;
388        kind.fieldList = Lists.newArrayList();
389        kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
390        return kind;
391    }
392
393    protected DataKind addDataKindNote(Context context) throws DefinitionException {
394        DataKind kind = addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, 110,
395                true));
396        kind.typeOverallMax = 1;
397        kind.actionHeader = new SimpleInflater(R.string.label_notes);
398        kind.actionBody = new SimpleInflater(Note.NOTE);
399        kind.fieldList = Lists.newArrayList();
400        kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
401
402        kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
403
404        return kind;
405    }
406
407    protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
408        DataKind kind = addKind(new DataKind(Website.CONTENT_ITEM_TYPE,
409                R.string.websiteLabelsGroup, 120, true));
410        kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
411        kind.actionBody = new SimpleInflater(Website.URL);
412        kind.defaultValues = new ContentValues();
413        kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
414
415        kind.fieldList = Lists.newArrayList();
416        kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
417
418        return kind;
419    }
420
421    protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
422        DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE,
423                    R.string.label_sip_address, 130, true));
424
425        kind.typeOverallMax = 1;
426        kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
427        kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
428        kind.fieldList = Lists.newArrayList();
429        kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
430                                         R.string.label_sip_address, FLAGS_SIP_ADDRESS));
431
432        return kind;
433    }
434
435    protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
436        DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
437                R.string.groupsLabel, 999, true));
438
439        kind.typeOverallMax = 1;
440        kind.fieldList = Lists.newArrayList();
441        kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
442
443        kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
444
445        return kind;
446    }
447
448    /**
449     * Simple inflater that assumes a string resource has a "%s" that will be
450     * filled from the given column.
451     */
452    public static class SimpleInflater implements StringInflater {
453        private final int mStringRes;
454        private final String mColumnName;
455
456        public SimpleInflater(int stringRes) {
457            this(stringRes, null);
458        }
459
460        public SimpleInflater(String columnName) {
461            this(-1, columnName);
462        }
463
464        public SimpleInflater(int stringRes, String columnName) {
465            mStringRes = stringRes;
466            mColumnName = columnName;
467        }
468
469        @Override
470        public CharSequence inflateUsing(Context context, ContentValues values) {
471            final boolean validColumn = values.containsKey(mColumnName);
472            final boolean validString = mStringRes > 0;
473
474            final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
475            final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
476
477            if (validString && validColumn) {
478                return String.format(stringValue.toString(), columnValue);
479            } else if (validString) {
480                return stringValue;
481            } else if (validColumn) {
482                return columnValue;
483            } else {
484                return null;
485            }
486        }
487
488        @Override
489        public String toString() {
490            return this.getClass().getSimpleName()
491                    + " mStringRes=" + mStringRes
492                    + " mColumnName" + mColumnName;
493        }
494
495        @NeededForTesting
496        public String getColumnNameForTest() {
497            return mColumnName;
498        }
499    }
500
501    public static abstract class CommonInflater implements StringInflater {
502        protected abstract int getTypeLabelResource(Integer type);
503
504        protected boolean isCustom(Integer type) {
505            return type == BaseTypes.TYPE_CUSTOM;
506        }
507
508        protected String getTypeColumn() {
509            return Phone.TYPE;
510        }
511
512        protected String getLabelColumn() {
513            return Phone.LABEL;
514        }
515
516        protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
517            final int labelRes = getTypeLabelResource(type);
518            if (type == null) {
519                return res.getText(labelRes);
520            } else if (isCustom(type)) {
521                return res.getString(labelRes, label == null ? "" : label);
522            } else {
523                return res.getText(labelRes);
524            }
525        }
526
527        @Override
528        public CharSequence inflateUsing(Context context, ContentValues values) {
529            final Integer type = values.getAsInteger(getTypeColumn());
530            final String label = values.getAsString(getLabelColumn());
531            return getTypeLabel(context.getResources(), type, label);
532        }
533
534        @Override
535        public String toString() {
536            return this.getClass().getSimpleName();
537        }
538    }
539
540    public static class PhoneActionInflater extends CommonInflater {
541        @Override
542        protected boolean isCustom(Integer type) {
543            return ContactDisplayUtils.isCustomPhoneType(type);
544        }
545
546        @Override
547        protected int getTypeLabelResource(Integer type) {
548            return ContactDisplayUtils.getPhoneLabelResourceId(type);
549        }
550    }
551
552    public static class PhoneActionAltInflater extends CommonInflater {
553        @Override
554        protected boolean isCustom(Integer type) {
555            return ContactDisplayUtils.isCustomPhoneType(type);
556        }
557
558        @Override
559        protected int getTypeLabelResource(Integer type) {
560            return ContactDisplayUtils.getSmsLabelResourceId(type);
561        }
562    }
563
564    public static class EmailActionInflater extends CommonInflater {
565        @Override
566        protected int getTypeLabelResource(Integer type) {
567            if (type == null) return R.string.email;
568            switch (type) {
569                case Email.TYPE_HOME: return R.string.email_home;
570                case Email.TYPE_WORK: return R.string.email_work;
571                case Email.TYPE_OTHER: return R.string.email_other;
572                case Email.TYPE_MOBILE: return R.string.email_mobile;
573                default: return R.string.email_custom;
574            }
575        }
576    }
577
578    public static class EventActionInflater extends CommonInflater {
579        @Override
580        protected int getTypeLabelResource(Integer type) {
581            return Event.getTypeResource(type);
582        }
583    }
584
585    public static class RelationActionInflater extends CommonInflater {
586        @Override
587        protected int getTypeLabelResource(Integer type) {
588            return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
589        }
590    }
591
592    public static class PostalActionInflater extends CommonInflater {
593        @Override
594        protected int getTypeLabelResource(Integer type) {
595            if (type == null) return R.string.map_other;
596            switch (type) {
597                case StructuredPostal.TYPE_HOME: return R.string.map_home;
598                case StructuredPostal.TYPE_WORK: return R.string.map_work;
599                case StructuredPostal.TYPE_OTHER: return R.string.map_other;
600                default: return R.string.map_custom;
601            }
602        }
603    }
604
605    public static class ImActionInflater extends CommonInflater {
606        @Override
607        protected String getTypeColumn() {
608            return Im.PROTOCOL;
609        }
610
611        @Override
612        protected String getLabelColumn() {
613            return Im.CUSTOM_PROTOCOL;
614        }
615
616        @Override
617        protected int getTypeLabelResource(Integer type) {
618            if (type == null) return R.string.chat;
619            switch (type) {
620                case Im.PROTOCOL_AIM: return R.string.chat_aim;
621                case Im.PROTOCOL_MSN: return R.string.chat_msn;
622                case Im.PROTOCOL_YAHOO: return R.string.chat_yahoo;
623                case Im.PROTOCOL_SKYPE: return R.string.chat_skype;
624                case Im.PROTOCOL_QQ: return R.string.chat_qq;
625                case Im.PROTOCOL_GOOGLE_TALK: return R.string.chat_gtalk;
626                case Im.PROTOCOL_ICQ: return R.string.chat_icq;
627                case Im.PROTOCOL_JABBER: return R.string.chat_jabber;
628                case Im.PROTOCOL_NETMEETING: return R.string.chat;
629                default: return R.string.chat;
630            }
631        }
632    }
633
634    @Override
635    public boolean isGroupMembershipEditable() {
636        return false;
637    }
638
639    /**
640     * Parses the content of the EditSchema tag in contacts.xml.
641     */
642    protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
643            throws XmlPullParserException, IOException, DefinitionException {
644
645        final int outerDepth = parser.getDepth();
646        int type;
647        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
648                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
649            final int depth = parser.getDepth();
650            if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
651                continue; // Not direct child tag
652            }
653
654            final String tag = parser.getName();
655
656            if (Tag.DATA_KIND.equals(tag)) {
657                for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
658                    addKind(kind);
659                }
660            } else {
661                Log.w(TAG, "Skipping unknown tag " + tag);
662            }
663        }
664    }
665
666    // Utility methods to keep code shorter.
667    private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
668        return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
669    }
670
671    private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
672        return attrs.getAttributeIntValue(null, attribute, defaultValue);
673    }
674
675    private static String getAttr(AttributeSet attrs, String attribute) {
676        return attrs.getAttributeValue(null, attribute);
677    }
678
679    // TODO Extract it to its own class, and move all KindBuilders to it as well.
680    private static class KindParser {
681        public static final KindParser INSTANCE = new KindParser();
682
683        private final Map<String, KindBuilder> mBuilders = Maps.newHashMap();
684
685        private KindParser() {
686            addBuilder(new NameKindBuilder());
687            addBuilder(new NicknameKindBuilder());
688            addBuilder(new PhoneKindBuilder());
689            addBuilder(new EmailKindBuilder());
690            addBuilder(new StructuredPostalKindBuilder());
691            addBuilder(new ImKindBuilder());
692            addBuilder(new OrganizationKindBuilder());
693            addBuilder(new PhotoKindBuilder());
694            addBuilder(new NoteKindBuilder());
695            addBuilder(new WebsiteKindBuilder());
696            addBuilder(new SipAddressKindBuilder());
697            addBuilder(new GroupMembershipKindBuilder());
698            addBuilder(new EventKindBuilder());
699            addBuilder(new RelationshipKindBuilder());
700        }
701
702        private void addBuilder(KindBuilder builder) {
703            mBuilders.put(builder.getTagName(), builder);
704        }
705
706        /**
707         * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns
708         * {@link DataKind}s.  (Usually just one, but there are three for the "name" kind.)
709         *
710         * This method returns a list, because we need to add 3 kinds for the name data kind.
711         * (structured, display and phonetic)
712         */
713        public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser,
714                AttributeSet attrs)
715                throws DefinitionException, XmlPullParserException, IOException {
716            final String kind = getAttr(attrs, Attr.KIND);
717            final KindBuilder builder = mBuilders.get(kind);
718            if (builder != null) {
719                return builder.parseDataKind(context, parser, attrs);
720            } else {
721                throw new DefinitionException("Undefined data kind '" + kind + "'");
722            }
723        }
724    }
725
726    private static abstract class KindBuilder {
727
728        public abstract String getTagName();
729
730        /**
731         * DataKind tag parser specific to each kind.  Subclasses must implement it.
732         */
733        public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser,
734                AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException;
735
736        /**
737         * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind
738         * tag.
739         */
740        protected final DataKind newDataKind(Context context, XmlPullParser parser,
741                AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn,
742                int titleRes, int weight, StringInflater actionHeader, StringInflater actionBody)
743                throws DefinitionException, XmlPullParserException, IOException {
744
745            if (Log.isLoggable(TAG, Log.DEBUG)) {
746                Log.d(TAG, "Adding DataKind: " + mimeType);
747            }
748
749            final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
750            kind.typeColumn = typeColumn;
751            kind.actionHeader = actionHeader;
752            kind.actionBody = actionBody;
753            kind.fieldList = Lists.newArrayList();
754
755            // Get more information from the tag...
756            // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
757            if (!isPseudo) {
758                kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
759
760                // Process "Type" tags.
761                // If a kind has the type column, contacts.xml must have at least one type
762                // definition.  Otherwise, it mustn't have a type definition.
763                if (kind.typeColumn != null) {
764                    // Parse and add types.
765                    kind.typeList = Lists.newArrayList();
766                    parseTypes(context, parser, attrs, kind, true);
767                    if (kind.typeList.size() == 0) {
768                        throw new DefinitionException(
769                                "Kind " + kind.mimeType + " must have at least one type");
770                    }
771                } else {
772                    // Make sure it has no types.
773                    parseTypes(context, parser, attrs, kind, false /* can't have types */);
774                }
775            }
776
777            return kind;
778        }
779
780        /**
781         * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds
782         * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type,
783         * so throws {@link DefinitionException}.
784         */
785        private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs,
786                DataKind kind, boolean canHaveTypes)
787                throws DefinitionException, XmlPullParserException, IOException {
788            final int outerDepth = parser.getDepth();
789            int type;
790            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
791                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
792                final int depth = parser.getDepth();
793                if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
794                    continue; // Not direct child tag
795                }
796
797                final String tag = parser.getName();
798                if (Tag.TYPE.equals(tag)) {
799                    if (canHaveTypes) {
800                        kind.typeList.add(parseTypeTag(parser, attrs, kind));
801                    } else {
802                        throw new DefinitionException(
803                                "Kind " + kind.mimeType + " can't have types");
804                    }
805                } else {
806                    throw new DefinitionException("Unknown tag: " + tag);
807                }
808            }
809        }
810
811        /**
812         * Parses a single Type element and returns an {@link EditType} built from it.  Uses
813         * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an
814         * {@link EditType}.
815         */
816        private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
817                throws DefinitionException {
818
819            final String typeName = getAttr(attrs, Attr.TYPE);
820
821            final EditType et = buildEditTypeForTypeTag(attrs, typeName);
822            if (et == null) {
823                throw new DefinitionException(
824                        "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
825            }
826            et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
827
828            return et;
829        }
830
831        /**
832         * Returns an {@link EditType} for the given "type".  Subclasses may optionally use
833         * the attributes in the tag to set optional values.
834         * (e.g. "yearOptional" for the event kind)
835         */
836        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
837            return null;
838        }
839
840        protected final void throwIfList(DataKind kind) throws DefinitionException {
841            if (kind.typeOverallMax != 1) {
842                throw new DefinitionException(
843                        "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
844            }
845        }
846    }
847
848    /**
849     * DataKind parser for Name. (structured, display, phonetic)
850     */
851    private static class NameKindBuilder extends KindBuilder {
852        @Override
853        public String getTagName() {
854            return "name";
855        }
856
857        private static void checkAttributeTrue(boolean value, String attrName)
858                throws DefinitionException {
859            if (!value) {
860                throw new DefinitionException(attrName + " must be true");
861            }
862        }
863
864        @Override
865        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
866                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
867                IOException {
868
869            // Build 3 data kinds:
870            // - StructuredName.CONTENT_ITEM_TYPE
871            // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
872            // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
873
874            final boolean displayOrderPrimary =
875                    context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
876
877            final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
878            final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
879            final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
880            final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
881            final boolean supportsPhoneticFamilyName =
882                    getAttr(attrs, "supportsPhoneticFamilyName", false);
883            final boolean supportsPhoneticMiddleName =
884                    getAttr(attrs, "supportsPhoneticMiddleName", false);
885            final boolean supportsPhoneticGivenName =
886                    getAttr(attrs, "supportsPhoneticGivenName", false);
887
888            // For now, every things must be supported.
889            checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
890            checkAttributeTrue(supportsPrefix, "supportsPrefix");
891            checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
892            checkAttributeTrue(supportsSuffix, "supportsSuffix");
893            checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
894            checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
895            checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
896
897            final List<DataKind> kinds = Lists.newArrayList();
898
899            // Structured name
900            final DataKind ks = newDataKind(context, parser, attrs, false,
901                    StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE,
902                    new SimpleInflater(R.string.nameLabelsGroup),
903                    new SimpleInflater(Nickname.NAME));
904
905            throwIfList(ks);
906            kinds.add(ks);
907
908            // Note about setLongForm/setShortForm below.
909            // We need to set this only when the type supports display name. (=supportsDisplayName)
910            // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
911            // "optional".
912
913            ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name,
914                    FLAGS_PERSON_NAME));
915            ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
916                    FLAGS_PERSON_NAME).setLongForm(true));
917            ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
918                    FLAGS_PERSON_NAME).setLongForm(true));
919            ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
920                    FLAGS_PERSON_NAME).setLongForm(true));
921            ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
922                    FLAGS_PERSON_NAME).setLongForm(true));
923            ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
924                    FLAGS_PERSON_NAME).setLongForm(true));
925            ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
926                    R.string.name_phonetic_family, FLAGS_PHONETIC));
927            ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
928                    R.string.name_phonetic_middle, FLAGS_PHONETIC));
929            ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
930                    R.string.name_phonetic_given, FLAGS_PHONETIC));
931
932            // Display name
933            final DataKind kd = newDataKind(context, parser, attrs, true,
934                    DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null,
935                    R.string.nameLabelsGroup, Weight.NONE,
936                    new SimpleInflater(R.string.nameLabelsGroup),
937                    new SimpleInflater(Nickname.NAME));
938            kd.typeOverallMax = 1;
939            kinds.add(kd);
940
941            kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
942                    R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
943
944            if (!displayOrderPrimary) {
945                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
946                        FLAGS_PERSON_NAME).setLongForm(true));
947                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
948                        FLAGS_PERSON_NAME).setLongForm(true));
949                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
950                        FLAGS_PERSON_NAME).setLongForm(true));
951                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
952                        FLAGS_PERSON_NAME).setLongForm(true));
953                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
954                        FLAGS_PERSON_NAME).setLongForm(true));
955            } else {
956                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
957                        FLAGS_PERSON_NAME).setLongForm(true));
958                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
959                        FLAGS_PERSON_NAME).setLongForm(true));
960                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
961                        FLAGS_PERSON_NAME).setLongForm(true));
962                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
963                        FLAGS_PERSON_NAME).setLongForm(true));
964                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
965                        FLAGS_PERSON_NAME).setLongForm(true));
966            }
967
968            // Phonetic name
969            final DataKind kp = newDataKind(context, parser, attrs, true,
970                    DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null,
971                    R.string.name_phonetic, Weight.NONE,
972                    new SimpleInflater(R.string.nameLabelsGroup),
973                    new SimpleInflater(Nickname.NAME));
974            kp.typeOverallMax = 1;
975            kinds.add(kp);
976
977            // We may want to change the order depending on displayOrderPrimary too.
978            kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
979                    R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
980            kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
981                    R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
982            kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
983                    R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
984            kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
985                    R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
986            return kinds;
987        }
988    }
989
990    private static class NicknameKindBuilder extends KindBuilder {
991        @Override
992        public String getTagName() {
993            return "nickname";
994        }
995
996        @Override
997        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
998                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
999                IOException {
1000            final DataKind kind = newDataKind(context, parser, attrs, false,
1001                    Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME,
1002                    new SimpleInflater(R.string.nicknameLabelsGroup),
1003                    new SimpleInflater(Nickname.NAME));
1004
1005            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
1006                    FLAGS_PERSON_NAME));
1007
1008            kind.defaultValues = new ContentValues();
1009            kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1010
1011            throwIfList(kind);
1012            return Lists.newArrayList(kind);
1013        }
1014    }
1015
1016    private static class PhoneKindBuilder extends KindBuilder {
1017        @Override
1018        public String getTagName() {
1019            return "phone";
1020        }
1021
1022        @Override
1023        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1024                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1025                IOException {
1026            final DataKind kind = newDataKind(context, parser, attrs, false,
1027                    Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE,
1028                    new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
1029
1030            kind.iconAltRes = R.drawable.ic_text_holo_light;
1031            kind.iconAltDescriptionRes = R.string.sms;
1032            kind.actionAltHeader = new PhoneActionAltInflater();
1033
1034            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
1035
1036            return Lists.newArrayList(kind);
1037        }
1038
1039        /** Just to avoid line-wrapping... */
1040        protected static EditType build(int type, boolean secondary) {
1041            return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
1042        }
1043
1044        @Override
1045        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1046            if ("home".equals(type)) return build(Phone.TYPE_HOME, false);
1047            if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false);
1048            if ("work".equals(type)) return build(Phone.TYPE_WORK, false);
1049            if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true);
1050            if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true);
1051            if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true);
1052            if ("other".equals(type)) return build(Phone.TYPE_OTHER, false);
1053            if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true);
1054            if ("car".equals(type)) return build(Phone.TYPE_CAR, true);
1055            if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true);
1056            if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true);
1057            if ("main".equals(type)) return build(Phone.TYPE_MAIN, true);
1058            if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true);
1059            if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true);
1060            if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true);
1061            if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true);
1062            if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true);
1063            if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true);
1064
1065            // Note "assistant" used to be a custom column for the fallback type, but not anymore.
1066            if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true);
1067            if ("mms".equals(type)) return build(Phone.TYPE_MMS, true);
1068            if ("custom".equals(type)) {
1069                return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
1070            }
1071            return null;
1072        }
1073    }
1074
1075    private static class EmailKindBuilder extends KindBuilder {
1076        @Override
1077        public String getTagName() {
1078            return "email";
1079        }
1080
1081        @Override
1082        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1083                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1084                IOException {
1085            final DataKind kind = newDataKind(context, parser, attrs, false,
1086                    Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL,
1087                    new EmailActionInflater(), new SimpleInflater(Email.DATA));
1088            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
1089
1090            return Lists.newArrayList(kind);
1091        }
1092
1093        @Override
1094        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1095            // EditType is mutable, so we need to create a new instance every time.
1096            if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME);
1097            if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK);
1098            if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER);
1099            if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE);
1100            if ("custom".equals(type)) {
1101                return buildEmailType(Email.TYPE_CUSTOM)
1102                        .setSecondary(true).setCustomColumn(Email.LABEL);
1103            }
1104            return null;
1105        }
1106    }
1107
1108    private static class StructuredPostalKindBuilder extends KindBuilder {
1109        @Override
1110        public String getTagName() {
1111            return "postal";
1112        }
1113
1114        @Override
1115        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1116                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1117                IOException {
1118            final DataKind kind = newDataKind(context, parser, attrs, false,
1119                    StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
1120                    R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL,
1121                    new PostalActionInflater(),
1122                    new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
1123
1124            if (getAttr(attrs, "needsStructured", false)) {
1125                if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
1126                    // Japanese order
1127                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
1128                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
1129                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
1130                            R.string.postal_postcode, FLAGS_POSTAL));
1131                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
1132                            R.string.postal_region, FLAGS_POSTAL));
1133                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
1134                            R.string.postal_city,FLAGS_POSTAL));
1135                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
1136                            R.string.postal_street, FLAGS_POSTAL));
1137                } else {
1138                    // Generic order
1139                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
1140                            R.string.postal_street, FLAGS_POSTAL));
1141                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
1142                            R.string.postal_city,FLAGS_POSTAL));
1143                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
1144                            R.string.postal_region, FLAGS_POSTAL));
1145                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
1146                            R.string.postal_postcode, FLAGS_POSTAL));
1147                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
1148                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
1149                }
1150            } else {
1151                kind.maxLinesForDisplay= MAX_LINES_FOR_POSTAL_ADDRESS;
1152                kind.fieldList.add(
1153                        new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
1154                                FLAGS_POSTAL));
1155            }
1156
1157            return Lists.newArrayList(kind);
1158        }
1159
1160        @Override
1161        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1162            // EditType is mutable, so we need to create a new instance every time.
1163            if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME);
1164            if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK);
1165            if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER);
1166            if ("custom".equals(type)) {
1167                return buildPostalType(StructuredPostal.TYPE_CUSTOM)
1168                        .setSecondary(true).setCustomColumn(Email.LABEL);
1169            }
1170            return null;
1171        }
1172    }
1173
1174    private static class ImKindBuilder extends KindBuilder {
1175        @Override
1176        public String getTagName() {
1177            return "im";
1178        }
1179
1180        @Override
1181        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1182                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1183                IOException {
1184
1185            // IM is special:
1186            // - It uses "protocol" as the custom label field
1187            // - Its TYPE is fixed to TYPE_OTHER
1188
1189            final DataKind kind = newDataKind(context, parser, attrs, false,
1190                    Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM,
1191                    new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action
1192                    );
1193            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
1194
1195            kind.defaultValues = new ContentValues();
1196            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
1197
1198            return Lists.newArrayList(kind);
1199        }
1200
1201        @Override
1202        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1203            if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM);
1204            if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN);
1205            if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO);
1206            if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE);
1207            if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ);
1208            if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK);
1209            if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ);
1210            if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER);
1211            if ("custom".equals(type)) {
1212                return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true)
1213                        .setCustomColumn(Im.CUSTOM_PROTOCOL);
1214            }
1215            return null;
1216        }
1217    }
1218
1219    private static class OrganizationKindBuilder extends KindBuilder {
1220        @Override
1221        public String getTagName() {
1222            return "organization";
1223        }
1224
1225        @Override
1226        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1227                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1228                IOException {
1229            final DataKind kind = newDataKind(context, parser, attrs, false,
1230                    Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup,
1231                    Weight.ORGANIZATION,
1232                    new SimpleInflater(Organization.COMPANY),
1233                    new SimpleInflater(Organization.TITLE));
1234
1235            kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
1236                    FLAGS_GENERIC_NAME));
1237            kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
1238                    FLAGS_GENERIC_NAME));
1239
1240            throwIfList(kind);
1241
1242            return Lists.newArrayList(kind);
1243        }
1244    }
1245
1246    private static class PhotoKindBuilder extends KindBuilder {
1247        @Override
1248        public String getTagName() {
1249            return "photo";
1250        }
1251
1252        @Override
1253        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1254                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1255                IOException {
1256            final DataKind kind = newDataKind(context, parser, attrs, false,
1257                    Photo.CONTENT_ITEM_TYPE, null /* no type */, Weight.NONE, -1,
1258                    null, null // no header, no body
1259                    );
1260
1261            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
1262
1263            throwIfList(kind);
1264
1265            return Lists.newArrayList(kind);
1266        }
1267    }
1268
1269    private static class NoteKindBuilder extends KindBuilder {
1270        @Override
1271        public String getTagName() {
1272            return "note";
1273        }
1274
1275        @Override
1276        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1277                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1278                IOException {
1279            final DataKind kind = newDataKind(context, parser, attrs, false,
1280                    Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE,
1281                    new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE));
1282
1283            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
1284            kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
1285
1286            throwIfList(kind);
1287
1288            return Lists.newArrayList(kind);
1289        }
1290    }
1291
1292    private static class WebsiteKindBuilder extends KindBuilder {
1293        @Override
1294        public String getTagName() {
1295            return "website";
1296        }
1297
1298        @Override
1299        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1300                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1301                IOException {
1302            final DataKind kind = newDataKind(context, parser, attrs, false,
1303                    Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE,
1304                    new SimpleInflater(R.string.websiteLabelsGroup),
1305                    new SimpleInflater(Website.URL));
1306
1307            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup,
1308                    FLAGS_WEBSITE));
1309
1310            kind.defaultValues = new ContentValues();
1311            kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
1312
1313            return Lists.newArrayList(kind);
1314        }
1315    }
1316
1317    private static class SipAddressKindBuilder extends KindBuilder {
1318        @Override
1319        public String getTagName() {
1320            return "sip_address";
1321        }
1322
1323        @Override
1324        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1325                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1326                IOException {
1327            final DataKind kind = newDataKind(context, parser, attrs, false,
1328                    SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address,
1329                    Weight.SIP_ADDRESS,
1330                    new SimpleInflater(R.string.label_sip_address),
1331                    new SimpleInflater(SipAddress.SIP_ADDRESS));
1332
1333            kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
1334                    R.string.label_sip_address, FLAGS_SIP_ADDRESS));
1335
1336            throwIfList(kind);
1337
1338            return Lists.newArrayList(kind);
1339        }
1340    }
1341
1342    private static class GroupMembershipKindBuilder extends KindBuilder {
1343        @Override
1344        public String getTagName() {
1345            return "group_membership";
1346        }
1347
1348        @Override
1349        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1350                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1351                IOException {
1352            final DataKind kind = newDataKind(context, parser, attrs, false,
1353                    GroupMembership.CONTENT_ITEM_TYPE, null,
1354                    R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, null, null);
1355
1356            kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
1357            kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
1358
1359            throwIfList(kind);
1360
1361            return Lists.newArrayList(kind);
1362        }
1363    }
1364
1365    /**
1366     * Event DataKind parser.
1367     *
1368     * Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
1369     */
1370    private static class EventKindBuilder extends KindBuilder {
1371        @Override
1372        public String getTagName() {
1373            return "event";
1374        }
1375
1376        @Override
1377        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1378                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1379                IOException {
1380            final DataKind kind = newDataKind(context, parser, attrs, false,
1381                    Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT,
1382                    new EventActionInflater(), new SimpleInflater(Event.START_DATE));
1383
1384            kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
1385
1386            if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
1387                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
1388                kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
1389            } else {
1390                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
1391                kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
1392            }
1393
1394            return Lists.newArrayList(kind);
1395        }
1396
1397        @Override
1398        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1399            final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
1400
1401            if ("birthday".equals(type)) {
1402                return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
1403            }
1404            if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo);
1405            if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo);
1406            if ("custom".equals(type)) {
1407                return buildEventType(Event.TYPE_CUSTOM, yo)
1408                        .setSecondary(true).setCustomColumn(Event.LABEL);
1409            }
1410            return null;
1411        }
1412    }
1413
1414    /**
1415     * Relationship DataKind parser.
1416     *
1417     * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
1418     * now.
1419     */
1420    private static class RelationshipKindBuilder extends KindBuilder {
1421        @Override
1422        public String getTagName() {
1423            return "relationship";
1424        }
1425
1426        @Override
1427        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1428                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1429                IOException {
1430            final DataKind kind = newDataKind(context, parser, attrs, false,
1431                    Relation.CONTENT_ITEM_TYPE, Relation.TYPE,
1432                    R.string.relationLabelsGroup, Weight.RELATIONSHIP,
1433                    new RelationActionInflater(), new SimpleInflater(Relation.NAME));
1434
1435            kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
1436                    FLAGS_RELATION));
1437
1438            kind.defaultValues = new ContentValues();
1439            kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
1440
1441            return Lists.newArrayList(kind);
1442        }
1443
1444        @Override
1445        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1446            // EditType is mutable, so we need to create a new instance every time.
1447            if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT);
1448            if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER);
1449            if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD);
1450            if ("domestic_partner".equals(type)) {
1451                    return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
1452            }
1453            if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER);
1454            if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND);
1455            if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER);
1456            if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER);
1457            if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT);
1458            if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER);
1459            if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY);
1460            if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE);
1461            if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER);
1462            if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE);
1463            if ("custom".equals(type)) {
1464                return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
1465                        .setCustomColumn(Relation.LABEL);
1466            }
1467            return null;
1468        }
1469    }
1470}
1471