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.testing.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    protected interface Weight {
103        static final int NONE = -1;
104        static final int PHONE = 10;
105        static final int EMAIL = 15;
106        static final int STRUCTURED_POSTAL = 25;
107        static final int NICKNAME = 111;
108        static final int EVENT = 120;
109        static final int ORGANIZATION = 125;
110        static final int NOTE = 130;
111        static final int IM = 140;
112        static final int SIP_ADDRESS = 145;
113        static final int GROUP_MEMBERSHIP = 150;
114        static final int WEBSITE = 160;
115        static final int RELATIONSHIP = 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_contacts_clr_48cv_44dp;
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, Weight.NONE, 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, 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        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, Weight.NONE, 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, Weight.NICKNAME, 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                Weight.PHONE, true));
259        kind.iconAltRes = R.drawable.ic_message_24dp;
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                Weight.EMAIL, 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, Weight.STRUCTURED_POSTAL, 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,
340                Weight.IM, 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, Weight.ORGANIZATION, true));
372        kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
373        kind.actionBody = ORGANIZATION_BODY_INFLATER;
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, Weight.NONE, 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,
395                Weight.NOTE, 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, Weight.WEBSITE, 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, Weight.SIP_ADDRESS, true));
424
425        kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
426        kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
427        kind.fieldList = Lists.newArrayList();
428        kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
429                                         R.string.label_sip_address, FLAGS_SIP_ADDRESS));
430        kind.typeOverallMax = 1;
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, Weight.GROUP_MEMBERSHIP, 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    public static final StringInflater ORGANIZATION_BODY_INFLATER = new StringInflater() {
635        @Override
636        public CharSequence inflateUsing(Context context, ContentValues values) {
637            final CharSequence companyValue = values.containsKey(Organization.COMPANY) ?
638                    values.getAsString(Organization.COMPANY) : null;
639            final CharSequence titleValue = values.containsKey(Organization.TITLE) ?
640                    values.getAsString(Organization.TITLE) : null;
641
642            if (companyValue != null && titleValue != null) {
643                return companyValue +  ": " + titleValue;
644            } else if (companyValue == null) {
645                return titleValue;
646            } else {
647                return companyValue;
648            }
649        }
650    };
651
652    @Override
653    public boolean isGroupMembershipEditable() {
654        return false;
655    }
656
657    /**
658     * Parses the content of the EditSchema tag in contacts.xml.
659     */
660    protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
661            throws XmlPullParserException, IOException, DefinitionException {
662
663        final int outerDepth = parser.getDepth();
664        int type;
665        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
666                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
667            final int depth = parser.getDepth();
668            if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
669                continue; // Not direct child tag
670            }
671
672            final String tag = parser.getName();
673
674            if (Tag.DATA_KIND.equals(tag)) {
675                for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
676                    addKind(kind);
677                }
678            } else {
679                Log.w(TAG, "Skipping unknown tag " + tag);
680            }
681        }
682    }
683
684    // Utility methods to keep code shorter.
685    private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
686        return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
687    }
688
689    private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
690        return attrs.getAttributeIntValue(null, attribute, defaultValue);
691    }
692
693    private static String getAttr(AttributeSet attrs, String attribute) {
694        return attrs.getAttributeValue(null, attribute);
695    }
696
697    // TODO Extract it to its own class, and move all KindBuilders to it as well.
698    private static class KindParser {
699        public static final KindParser INSTANCE = new KindParser();
700
701        private final Map<String, KindBuilder> mBuilders = Maps.newHashMap();
702
703        private KindParser() {
704            addBuilder(new NameKindBuilder());
705            addBuilder(new NicknameKindBuilder());
706            addBuilder(new PhoneKindBuilder());
707            addBuilder(new EmailKindBuilder());
708            addBuilder(new StructuredPostalKindBuilder());
709            addBuilder(new ImKindBuilder());
710            addBuilder(new OrganizationKindBuilder());
711            addBuilder(new PhotoKindBuilder());
712            addBuilder(new NoteKindBuilder());
713            addBuilder(new WebsiteKindBuilder());
714            addBuilder(new SipAddressKindBuilder());
715            addBuilder(new GroupMembershipKindBuilder());
716            addBuilder(new EventKindBuilder());
717            addBuilder(new RelationshipKindBuilder());
718        }
719
720        private void addBuilder(KindBuilder builder) {
721            mBuilders.put(builder.getTagName(), builder);
722        }
723
724        /**
725         * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns
726         * {@link DataKind}s.  (Usually just one, but there are three for the "name" kind.)
727         *
728         * This method returns a list, because we need to add 3 kinds for the name data kind.
729         * (structured, display and phonetic)
730         */
731        public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser,
732                AttributeSet attrs)
733                throws DefinitionException, XmlPullParserException, IOException {
734            final String kind = getAttr(attrs, Attr.KIND);
735            final KindBuilder builder = mBuilders.get(kind);
736            if (builder != null) {
737                return builder.parseDataKind(context, parser, attrs);
738            } else {
739                throw new DefinitionException("Undefined data kind '" + kind + "'");
740            }
741        }
742    }
743
744    private static abstract class KindBuilder {
745
746        public abstract String getTagName();
747
748        /**
749         * DataKind tag parser specific to each kind.  Subclasses must implement it.
750         */
751        public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser,
752                AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException;
753
754        /**
755         * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind
756         * tag.
757         */
758        protected final DataKind newDataKind(Context context, XmlPullParser parser,
759                AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn,
760                int titleRes, int weight, StringInflater actionHeader, StringInflater actionBody)
761                throws DefinitionException, XmlPullParserException, IOException {
762
763            if (Log.isLoggable(TAG, Log.DEBUG)) {
764                Log.d(TAG, "Adding DataKind: " + mimeType);
765            }
766
767            final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
768            kind.typeColumn = typeColumn;
769            kind.actionHeader = actionHeader;
770            kind.actionBody = actionBody;
771            kind.fieldList = Lists.newArrayList();
772
773            // Get more information from the tag...
774            // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
775            if (!isPseudo) {
776                kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
777
778                // Process "Type" tags.
779                // If a kind has the type column, contacts.xml must have at least one type
780                // definition.  Otherwise, it mustn't have a type definition.
781                if (kind.typeColumn != null) {
782                    // Parse and add types.
783                    kind.typeList = Lists.newArrayList();
784                    parseTypes(context, parser, attrs, kind, true);
785                    if (kind.typeList.size() == 0) {
786                        throw new DefinitionException(
787                                "Kind " + kind.mimeType + " must have at least one type");
788                    }
789                } else {
790                    // Make sure it has no types.
791                    parseTypes(context, parser, attrs, kind, false /* can't have types */);
792                }
793            }
794
795            return kind;
796        }
797
798        /**
799         * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds
800         * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type,
801         * so throws {@link DefinitionException}.
802         */
803        private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs,
804                DataKind kind, boolean canHaveTypes)
805                throws DefinitionException, XmlPullParserException, IOException {
806            final int outerDepth = parser.getDepth();
807            int type;
808            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
809                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
810                final int depth = parser.getDepth();
811                if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
812                    continue; // Not direct child tag
813                }
814
815                final String tag = parser.getName();
816                if (Tag.TYPE.equals(tag)) {
817                    if (canHaveTypes) {
818                        kind.typeList.add(parseTypeTag(parser, attrs, kind));
819                    } else {
820                        throw new DefinitionException(
821                                "Kind " + kind.mimeType + " can't have types");
822                    }
823                } else {
824                    throw new DefinitionException("Unknown tag: " + tag);
825                }
826            }
827        }
828
829        /**
830         * Parses a single Type element and returns an {@link EditType} built from it.  Uses
831         * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an
832         * {@link EditType}.
833         */
834        private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
835                throws DefinitionException {
836
837            final String typeName = getAttr(attrs, Attr.TYPE);
838
839            final EditType et = buildEditTypeForTypeTag(attrs, typeName);
840            if (et == null) {
841                throw new DefinitionException(
842                        "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
843            }
844            et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
845
846            return et;
847        }
848
849        /**
850         * Returns an {@link EditType} for the given "type".  Subclasses may optionally use
851         * the attributes in the tag to set optional values.
852         * (e.g. "yearOptional" for the event kind)
853         */
854        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
855            return null;
856        }
857
858        protected final void throwIfList(DataKind kind) throws DefinitionException {
859            if (kind.typeOverallMax != 1) {
860                throw new DefinitionException(
861                        "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
862            }
863        }
864    }
865
866    /**
867     * DataKind parser for Name. (structured, display, phonetic)
868     */
869    private static class NameKindBuilder extends KindBuilder {
870        @Override
871        public String getTagName() {
872            return "name";
873        }
874
875        private static void checkAttributeTrue(boolean value, String attrName)
876                throws DefinitionException {
877            if (!value) {
878                throw new DefinitionException(attrName + " must be true");
879            }
880        }
881
882        @Override
883        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
884                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
885                IOException {
886
887            // Build 3 data kinds:
888            // - StructuredName.CONTENT_ITEM_TYPE
889            // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
890            // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
891
892            final boolean displayOrderPrimary =
893                    context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
894
895            final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
896            final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
897            final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
898            final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
899            final boolean supportsPhoneticFamilyName =
900                    getAttr(attrs, "supportsPhoneticFamilyName", false);
901            final boolean supportsPhoneticMiddleName =
902                    getAttr(attrs, "supportsPhoneticMiddleName", false);
903            final boolean supportsPhoneticGivenName =
904                    getAttr(attrs, "supportsPhoneticGivenName", false);
905
906            // For now, every things must be supported.
907            checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
908            checkAttributeTrue(supportsPrefix, "supportsPrefix");
909            checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
910            checkAttributeTrue(supportsSuffix, "supportsSuffix");
911            checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
912            checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
913            checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
914
915            final List<DataKind> kinds = Lists.newArrayList();
916
917            // Structured name
918            final DataKind ks = newDataKind(context, parser, attrs, false,
919                    StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE,
920                    new SimpleInflater(R.string.nameLabelsGroup),
921                    new SimpleInflater(Nickname.NAME));
922
923            throwIfList(ks);
924            kinds.add(ks);
925
926            // Note about setLongForm/setShortForm below.
927            // We need to set this only when the type supports display name. (=supportsDisplayName)
928            // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
929            // "optional".
930
931            ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name,
932                    FLAGS_PERSON_NAME));
933            ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
934                    FLAGS_PERSON_NAME).setLongForm(true));
935            ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
936                    FLAGS_PERSON_NAME).setLongForm(true));
937            ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
938                    FLAGS_PERSON_NAME).setLongForm(true));
939            ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
940                    FLAGS_PERSON_NAME).setLongForm(true));
941            ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
942                    FLAGS_PERSON_NAME).setLongForm(true));
943            ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
944                    R.string.name_phonetic_family, FLAGS_PHONETIC));
945            ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
946                    R.string.name_phonetic_middle, FLAGS_PHONETIC));
947            ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
948                    R.string.name_phonetic_given, FLAGS_PHONETIC));
949
950            // Display name
951            final DataKind kd = newDataKind(context, parser, attrs, true,
952                    DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null,
953                    R.string.nameLabelsGroup, Weight.NONE,
954                    new SimpleInflater(R.string.nameLabelsGroup),
955                    new SimpleInflater(Nickname.NAME));
956            kd.typeOverallMax = 1;
957            kinds.add(kd);
958
959            kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
960                    R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
961
962            if (!displayOrderPrimary) {
963                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
964                        FLAGS_PERSON_NAME).setLongForm(true));
965                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
966                        FLAGS_PERSON_NAME).setLongForm(true));
967                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
968                        FLAGS_PERSON_NAME).setLongForm(true));
969                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
970                        FLAGS_PERSON_NAME).setLongForm(true));
971                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
972                        FLAGS_PERSON_NAME).setLongForm(true));
973            } else {
974                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
975                        FLAGS_PERSON_NAME).setLongForm(true));
976                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
977                        FLAGS_PERSON_NAME).setLongForm(true));
978                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
979                        FLAGS_PERSON_NAME).setLongForm(true));
980                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
981                        FLAGS_PERSON_NAME).setLongForm(true));
982                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
983                        FLAGS_PERSON_NAME).setLongForm(true));
984            }
985
986            // Phonetic name
987            final DataKind kp = newDataKind(context, parser, attrs, true,
988                    DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null,
989                    R.string.name_phonetic, Weight.NONE,
990                    new SimpleInflater(R.string.nameLabelsGroup),
991                    new SimpleInflater(Nickname.NAME));
992            kp.typeOverallMax = 1;
993            kinds.add(kp);
994
995            // We may want to change the order depending on displayOrderPrimary too.
996            kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
997                    R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
998            kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
999                    R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
1000            kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
1001                    R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
1002            kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
1003                    R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
1004            return kinds;
1005        }
1006    }
1007
1008    private static class NicknameKindBuilder extends KindBuilder {
1009        @Override
1010        public String getTagName() {
1011            return "nickname";
1012        }
1013
1014        @Override
1015        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1016                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1017                IOException {
1018            final DataKind kind = newDataKind(context, parser, attrs, false,
1019                    Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME,
1020                    new SimpleInflater(R.string.nicknameLabelsGroup),
1021                    new SimpleInflater(Nickname.NAME));
1022
1023            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
1024                    FLAGS_PERSON_NAME));
1025
1026            kind.defaultValues = new ContentValues();
1027            kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1028
1029            throwIfList(kind);
1030            return Lists.newArrayList(kind);
1031        }
1032    }
1033
1034    private static class PhoneKindBuilder extends KindBuilder {
1035        @Override
1036        public String getTagName() {
1037            return "phone";
1038        }
1039
1040        @Override
1041        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1042                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1043                IOException {
1044            final DataKind kind = newDataKind(context, parser, attrs, false,
1045                    Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE,
1046                    new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
1047
1048            kind.iconAltRes = R.drawable.ic_message_24dp;
1049            kind.iconAltDescriptionRes = R.string.sms;
1050            kind.actionAltHeader = new PhoneActionAltInflater();
1051
1052            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
1053
1054            return Lists.newArrayList(kind);
1055        }
1056
1057        /** Just to avoid line-wrapping... */
1058        protected static EditType build(int type, boolean secondary) {
1059            return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
1060        }
1061
1062        @Override
1063        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1064            if ("home".equals(type)) return build(Phone.TYPE_HOME, false);
1065            if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false);
1066            if ("work".equals(type)) return build(Phone.TYPE_WORK, false);
1067            if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true);
1068            if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true);
1069            if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true);
1070            if ("other".equals(type)) return build(Phone.TYPE_OTHER, false);
1071            if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true);
1072            if ("car".equals(type)) return build(Phone.TYPE_CAR, true);
1073            if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true);
1074            if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true);
1075            if ("main".equals(type)) return build(Phone.TYPE_MAIN, true);
1076            if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true);
1077            if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true);
1078            if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true);
1079            if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true);
1080            if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true);
1081            if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true);
1082
1083            // Note "assistant" used to be a custom column for the fallback type, but not anymore.
1084            if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true);
1085            if ("mms".equals(type)) return build(Phone.TYPE_MMS, true);
1086            if ("custom".equals(type)) {
1087                return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
1088            }
1089            return null;
1090        }
1091    }
1092
1093    private static class EmailKindBuilder extends KindBuilder {
1094        @Override
1095        public String getTagName() {
1096            return "email";
1097        }
1098
1099        @Override
1100        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1101                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1102                IOException {
1103            final DataKind kind = newDataKind(context, parser, attrs, false,
1104                    Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL,
1105                    new EmailActionInflater(), new SimpleInflater(Email.DATA));
1106            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
1107
1108            return Lists.newArrayList(kind);
1109        }
1110
1111        @Override
1112        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1113            // EditType is mutable, so we need to create a new instance every time.
1114            if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME);
1115            if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK);
1116            if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER);
1117            if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE);
1118            if ("custom".equals(type)) {
1119                return buildEmailType(Email.TYPE_CUSTOM)
1120                        .setSecondary(true).setCustomColumn(Email.LABEL);
1121            }
1122            return null;
1123        }
1124    }
1125
1126    private static class StructuredPostalKindBuilder extends KindBuilder {
1127        @Override
1128        public String getTagName() {
1129            return "postal";
1130        }
1131
1132        @Override
1133        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1134                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1135                IOException {
1136            final DataKind kind = newDataKind(context, parser, attrs, false,
1137                    StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
1138                    R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL,
1139                    new PostalActionInflater(),
1140                    new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
1141
1142            if (getAttr(attrs, "needsStructured", false)) {
1143                if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
1144                    // Japanese order
1145                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
1146                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
1147                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
1148                            R.string.postal_postcode, FLAGS_POSTAL));
1149                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
1150                            R.string.postal_region, FLAGS_POSTAL));
1151                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
1152                            R.string.postal_city,FLAGS_POSTAL));
1153                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
1154                            R.string.postal_street, FLAGS_POSTAL));
1155                } else {
1156                    // Generic order
1157                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
1158                            R.string.postal_street, FLAGS_POSTAL));
1159                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
1160                            R.string.postal_city,FLAGS_POSTAL));
1161                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
1162                            R.string.postal_region, FLAGS_POSTAL));
1163                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
1164                            R.string.postal_postcode, FLAGS_POSTAL));
1165                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
1166                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
1167                }
1168            } else {
1169                kind.maxLinesForDisplay= MAX_LINES_FOR_POSTAL_ADDRESS;
1170                kind.fieldList.add(
1171                        new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
1172                                FLAGS_POSTAL));
1173            }
1174
1175            return Lists.newArrayList(kind);
1176        }
1177
1178        @Override
1179        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1180            // EditType is mutable, so we need to create a new instance every time.
1181            if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME);
1182            if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK);
1183            if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER);
1184            if ("custom".equals(type)) {
1185                return buildPostalType(StructuredPostal.TYPE_CUSTOM)
1186                        .setSecondary(true).setCustomColumn(Email.LABEL);
1187            }
1188            return null;
1189        }
1190    }
1191
1192    private static class ImKindBuilder extends KindBuilder {
1193        @Override
1194        public String getTagName() {
1195            return "im";
1196        }
1197
1198        @Override
1199        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1200                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1201                IOException {
1202
1203            // IM is special:
1204            // - It uses "protocol" as the custom label field
1205            // - Its TYPE is fixed to TYPE_OTHER
1206
1207            final DataKind kind = newDataKind(context, parser, attrs, false,
1208                    Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM,
1209                    new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action
1210                    );
1211            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
1212
1213            kind.defaultValues = new ContentValues();
1214            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
1215
1216            return Lists.newArrayList(kind);
1217        }
1218
1219        @Override
1220        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1221            if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM);
1222            if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN);
1223            if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO);
1224            if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE);
1225            if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ);
1226            if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK);
1227            if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ);
1228            if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER);
1229            if ("custom".equals(type)) {
1230                return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true)
1231                        .setCustomColumn(Im.CUSTOM_PROTOCOL);
1232            }
1233            return null;
1234        }
1235    }
1236
1237    private static class OrganizationKindBuilder extends KindBuilder {
1238        @Override
1239        public String getTagName() {
1240            return "organization";
1241        }
1242
1243        @Override
1244        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1245                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1246                IOException {
1247            final DataKind kind = newDataKind(context, parser, attrs, false,
1248                    Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup,
1249                    Weight.ORGANIZATION,
1250                    new SimpleInflater(R.string.organizationLabelsGroup),
1251                    ORGANIZATION_BODY_INFLATER);
1252
1253            kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
1254                    FLAGS_GENERIC_NAME));
1255            kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
1256                    FLAGS_GENERIC_NAME));
1257
1258            throwIfList(kind);
1259
1260            return Lists.newArrayList(kind);
1261        }
1262    }
1263
1264    private static class PhotoKindBuilder extends KindBuilder {
1265        @Override
1266        public String getTagName() {
1267            return "photo";
1268        }
1269
1270        @Override
1271        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1272                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1273                IOException {
1274            final DataKind kind = newDataKind(context, parser, attrs, false,
1275                    Photo.CONTENT_ITEM_TYPE, null /* no type */, Weight.NONE, -1,
1276                    null, null // no header, no body
1277                    );
1278
1279            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
1280
1281            throwIfList(kind);
1282
1283            return Lists.newArrayList(kind);
1284        }
1285    }
1286
1287    private static class NoteKindBuilder extends KindBuilder {
1288        @Override
1289        public String getTagName() {
1290            return "note";
1291        }
1292
1293        @Override
1294        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1295                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1296                IOException {
1297            final DataKind kind = newDataKind(context, parser, attrs, false,
1298                    Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE,
1299                    new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE));
1300
1301            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
1302            kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
1303
1304            throwIfList(kind);
1305
1306            return Lists.newArrayList(kind);
1307        }
1308    }
1309
1310    private static class WebsiteKindBuilder extends KindBuilder {
1311        @Override
1312        public String getTagName() {
1313            return "website";
1314        }
1315
1316        @Override
1317        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1318                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1319                IOException {
1320            final DataKind kind = newDataKind(context, parser, attrs, false,
1321                    Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE,
1322                    new SimpleInflater(R.string.websiteLabelsGroup),
1323                    new SimpleInflater(Website.URL));
1324
1325            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup,
1326                    FLAGS_WEBSITE));
1327
1328            kind.defaultValues = new ContentValues();
1329            kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
1330
1331            return Lists.newArrayList(kind);
1332        }
1333    }
1334
1335    private static class SipAddressKindBuilder extends KindBuilder {
1336        @Override
1337        public String getTagName() {
1338            return "sip_address";
1339        }
1340
1341        @Override
1342        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1343                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1344                IOException {
1345            final DataKind kind = newDataKind(context, parser, attrs, false,
1346                    SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address,
1347                    Weight.SIP_ADDRESS,
1348                    new SimpleInflater(R.string.label_sip_address),
1349                    new SimpleInflater(SipAddress.SIP_ADDRESS));
1350
1351            kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
1352                    R.string.label_sip_address, FLAGS_SIP_ADDRESS));
1353
1354            throwIfList(kind);
1355
1356            return Lists.newArrayList(kind);
1357        }
1358    }
1359
1360    private static class GroupMembershipKindBuilder extends KindBuilder {
1361        @Override
1362        public String getTagName() {
1363            return "group_membership";
1364        }
1365
1366        @Override
1367        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1368                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1369                IOException {
1370            final DataKind kind = newDataKind(context, parser, attrs, false,
1371                    GroupMembership.CONTENT_ITEM_TYPE, null,
1372                    R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, null, null);
1373
1374            kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
1375            kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
1376
1377            throwIfList(kind);
1378
1379            return Lists.newArrayList(kind);
1380        }
1381    }
1382
1383    /**
1384     * Event DataKind parser.
1385     *
1386     * Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
1387     */
1388    private static class EventKindBuilder extends KindBuilder {
1389        @Override
1390        public String getTagName() {
1391            return "event";
1392        }
1393
1394        @Override
1395        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1396                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1397                IOException {
1398            final DataKind kind = newDataKind(context, parser, attrs, false,
1399                    Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT,
1400                    new EventActionInflater(), new SimpleInflater(Event.START_DATE));
1401
1402            kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
1403
1404            if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
1405                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
1406                kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
1407            } else {
1408                kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
1409                kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
1410            }
1411
1412            return Lists.newArrayList(kind);
1413        }
1414
1415        @Override
1416        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1417            final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
1418
1419            if ("birthday".equals(type)) {
1420                return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
1421            }
1422            if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo);
1423            if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo);
1424            if ("custom".equals(type)) {
1425                return buildEventType(Event.TYPE_CUSTOM, yo)
1426                        .setSecondary(true).setCustomColumn(Event.LABEL);
1427            }
1428            return null;
1429        }
1430    }
1431
1432    /**
1433     * Relationship DataKind parser.
1434     *
1435     * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
1436     * now.
1437     */
1438    private static class RelationshipKindBuilder extends KindBuilder {
1439        @Override
1440        public String getTagName() {
1441            return "relationship";
1442        }
1443
1444        @Override
1445        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
1446                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
1447                IOException {
1448            final DataKind kind = newDataKind(context, parser, attrs, false,
1449                    Relation.CONTENT_ITEM_TYPE, Relation.TYPE,
1450                    R.string.relationLabelsGroup, Weight.RELATIONSHIP,
1451                    new RelationActionInflater(), new SimpleInflater(Relation.NAME));
1452
1453            kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
1454                    FLAGS_RELATION));
1455
1456            kind.defaultValues = new ContentValues();
1457            kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
1458
1459            return Lists.newArrayList(kind);
1460        }
1461
1462        @Override
1463        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
1464            // EditType is mutable, so we need to create a new instance every time.
1465            if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT);
1466            if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER);
1467            if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD);
1468            if ("domestic_partner".equals(type)) {
1469                    return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
1470            }
1471            if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER);
1472            if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND);
1473            if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER);
1474            if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER);
1475            if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT);
1476            if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER);
1477            if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY);
1478            if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE);
1479            if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER);
1480            if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE);
1481            if ("custom".equals(type)) {
1482                return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
1483                        .setCustomColumn(Relation.LABEL);
1484            }
1485            return null;
1486        }
1487    }
1488}
1489