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