Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.Comparator;
10 import java.util.HashMap;
11 import java.util.List;
12 import java.util.Map.Entry;
14 import org.json.JSONArray;
15 import org.json.JSONException;
16 import org.json.JSONObject;
17 import org.mozilla.gecko.util.GeckoEventListener;
18 import org.mozilla.gecko.util.ThreadUtils;
20 import android.accounts.Account;
21 import android.accounts.AccountManager;
22 import android.app.AlertDialog;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.DialogInterface;
29 import android.content.OperationApplicationException;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.Build;
33 import android.os.RemoteException;
34 import android.provider.ContactsContract;
35 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
36 import android.provider.ContactsContract.CommonDataKinds.Email;
37 import android.provider.ContactsContract.CommonDataKinds.Event;
38 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
39 import android.provider.ContactsContract.CommonDataKinds.Im;
40 import android.provider.ContactsContract.CommonDataKinds.Nickname;
41 import android.provider.ContactsContract.CommonDataKinds.Note;
42 import android.provider.ContactsContract.CommonDataKinds.Organization;
43 import android.provider.ContactsContract.CommonDataKinds.Phone;
44 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
45 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
46 import android.provider.ContactsContract.CommonDataKinds.Website;
47 import android.provider.ContactsContract.Data;
48 import android.provider.ContactsContract.Groups;
49 import android.provider.ContactsContract.RawContacts;
50 import android.provider.ContactsContract.RawContacts.Entity;
51 import android.telephony.PhoneNumberUtils;
52 import android.util.Log;
54 public class ContactService implements GeckoEventListener {
55 private static final String LOGTAG = "GeckoContactService";
56 private static final boolean DEBUG = false;
58 private final static int GROUP_ACCOUNT_NAME = 0;
59 private final static int GROUP_ACCOUNT_TYPE = 1;
60 private final static int GROUP_ID = 2;
61 private final static int GROUP_TITLE = 3;
62 private final static int GROUP_AUTO_ADD = 4;
64 private final static String CARRIER_COLUMN = Data.DATA5;
65 private final static String CUSTOM_DATA_COLUMN = Data.DATA1;
67 // Pre-Honeycomb versions of Android have a "My Contacts" system group that all contacts are
68 // assigned to by default for a given account. After Honeycomb, an AUTO_ADD database column
69 // was added to denote groups that contacts are automatically added to
70 private final static String PRE_HONEYCOMB_DEFAULT_GROUP = "System Group: My Contacts";
71 private final static String MIMETYPE_ADDITIONAL_NAME = "org.mozilla.gecko/additional_name";
72 private final static String MIMETYPE_SEX = "org.mozilla.gecko/sex";
73 private final static String MIMETYPE_GENDER_IDENTITY = "org.mozilla.gecko/gender_identity";
74 private final static String MIMETYPE_KEY = "org.mozilla.gecko/key";
75 private final static String MIMETYPE_MOZILLA_CONTACTS_FLAG = "org.mozilla.gecko/contact_flag";
77 private final EventDispatcher mEventDispatcher;
79 private String mAccountName;
80 private String mAccountType;
81 private String mGroupTitle;
82 private long mGroupId;
83 private boolean mGotDeviceAccount;
85 private HashMap<String, String> mColumnNameConstantsMap;
86 private HashMap<String, String> mMimeTypeConstantsMap;
87 private HashMap<String, Integer> mAddressTypesMap;
88 private HashMap<String, Integer> mPhoneTypesMap;
89 private HashMap<String, Integer> mEmailTypesMap;
90 private HashMap<String, Integer> mWebsiteTypesMap;
91 private HashMap<String, Integer> mImTypesMap;
93 private ContentResolver mContentResolver;
94 private GeckoApp mActivity;
96 ContactService(EventDispatcher eventDispatcher, GeckoApp activity) {
97 mEventDispatcher = eventDispatcher;
98 mActivity = activity;
99 mContentResolver = mActivity.getContentResolver();
100 mGotDeviceAccount = false;
102 registerEventListener("Android:Contacts:Clear");
103 registerEventListener("Android:Contacts:Find");
104 registerEventListener("Android:Contacts:GetAll");
105 registerEventListener("Android:Contacts:GetCount");
106 registerEventListener("Android:Contact:Remove");
107 registerEventListener("Android:Contact:Save");
108 }
110 public void destroy() {
111 unregisterEventListener("Android:Contacts:Clear");
112 unregisterEventListener("Android:Contacts:Find");
113 unregisterEventListener("Android:Contacts:GetAll");
114 unregisterEventListener("Android:Contacts:GetCount");
115 unregisterEventListener("Android:Contact:Remove");
116 unregisterEventListener("Android:Contact:Save");
117 }
119 @Override
120 public void handleMessage(final String event, final JSONObject message) {
121 // If the account chooser dialog needs shown to the user, the message handling becomes
122 // asychronous so it needs posted to a background thread from the UI thread when the
123 // account chooser dialog is dismissed by the user.
124 Runnable handleMessage = new Runnable() {
125 @Override
126 public void run() {
127 try {
128 if (DEBUG) {
129 Log.d(LOGTAG, "Event: " + event + "\nMessage: " + message.toString(3));
130 }
132 final JSONObject messageData = message.getJSONObject("data");
133 final String requestID = messageData.getString("requestID");
135 // Options may not exist for all operations
136 JSONObject contactOptions = messageData.optJSONObject("options");
138 if ("Android:Contacts:Find".equals(event)) {
139 findContacts(contactOptions, requestID);
140 } else if ("Android:Contacts:GetAll".equals(event)) {
141 getAllContacts(messageData, requestID);
142 } else if ("Android:Contacts:Clear".equals(event)) {
143 clearAllContacts(contactOptions, requestID);
144 } else if ("Android:Contact:Save".equals(event)) {
145 saveContact(contactOptions, requestID);
146 } else if ("Android:Contact:Remove".equals(event)) {
147 removeContact(contactOptions, requestID);
148 } else if ("Android:Contacts:GetCount".equals(event)) {
149 getContactsCount(requestID);
150 } else {
151 throw new IllegalArgumentException("Unexpected event: " + event);
152 }
153 } catch (JSONException e) {
154 throw new IllegalArgumentException("Message: " + e);
155 }
156 }
157 };
159 // Get the account name/type if they haven't been set yet
160 if (!mGotDeviceAccount) {
161 getDeviceAccount(handleMessage);
162 } else {
163 handleMessage.run();
164 }
165 }
167 private void findContacts(final JSONObject contactOptions, final String requestID) {
168 long[] rawContactIds = findContactsRawIds(contactOptions);
169 Log.i(LOGTAG, "Got " + (rawContactIds != null ? rawContactIds.length : "null") + " raw contact IDs");
171 final String[] sortOptions = getSortOptionsFromJSON(contactOptions);
173 if (rawContactIds == null || sortOptions == null) {
174 sendCallbackToJavascript("Android:Contacts:Find:Return:KO", requestID, null, null);
175 } else {
176 sendCallbackToJavascript("Android:Contacts:Find:Return:OK", requestID,
177 new String[] {"contacts"},
178 new Object[] {getContactsAsJSONArray(rawContactIds, sortOptions[0],
179 sortOptions[1])});
180 }
181 }
183 private void getAllContacts(final JSONObject contactOptions, final String requestID) {
184 long[] rawContactIds = getAllRawContactIds();
185 Log.i(LOGTAG, "Got " + rawContactIds.length + " raw contact IDs");
187 final String[] sortOptions = getSortOptionsFromJSON(contactOptions);
189 if (rawContactIds == null || sortOptions == null) {
190 // There's no failure message for getAll
191 return;
192 } else {
193 sendCallbackToJavascript("Android:Contacts:GetAll:Next", requestID,
194 new String[] {"contacts"},
195 new Object[] {getContactsAsJSONArray(rawContactIds, sortOptions[0],
196 sortOptions[1])});
197 }
198 }
200 private static String[] getSortOptionsFromJSON(final JSONObject contactOptions) {
201 String sortBy = null;
202 String sortOrder = null;
204 try {
205 final JSONObject findOptions = contactOptions.getJSONObject("findOptions");
206 sortBy = findOptions.optString("sortBy").toLowerCase();
207 sortOrder = findOptions.optString("sortOrder").toLowerCase();
209 if ("".equals(sortBy)) {
210 sortBy = null;
211 }
212 if ("".equals(sortOrder)) {
213 sortOrder = "ascending";
214 }
216 // Only "familyname" and "givenname" are valid sortBy values and only "ascending"
217 // and "descending" are valid sortOrder values
218 if ((sortBy != null && !"familyname".equals(sortBy) && !"givenname".equals(sortBy)) ||
219 (!"ascending".equals(sortOrder) && !"descending".equals(sortOrder))) {
220 return null;
221 }
222 } catch (JSONException e) {
223 throw new IllegalArgumentException(e);
224 }
226 return new String[] {sortBy, sortOrder};
227 }
229 private long[] findContactsRawIds(final JSONObject contactOptions) {
230 List<Long> rawContactIds = new ArrayList<Long>();
231 Cursor cursor = null;
233 try {
234 final JSONObject findOptions = contactOptions.getJSONObject("findOptions");
235 String filterValue = findOptions.optString("filterValue");
236 JSONArray filterBy = findOptions.optJSONArray("filterBy");
237 final String filterOp = findOptions.optString("filterOp");
238 final int filterLimit = findOptions.getInt("filterLimit");
239 final int substringMatching = findOptions.getInt("substringMatching");
241 // If filter value is undefined, avoid all the logic below and just return
242 // all available raw contact IDs
243 if ("".equals(filterValue) || "".equals(filterOp)) {
244 long[] allRawContactIds = getAllRawContactIds();
246 // Truncate the raw contacts IDs array if necessary
247 if (filterLimit > 0 && allRawContactIds.length > filterLimit) {
248 long[] truncatedRawContactIds = new long[filterLimit];
249 for (int i = 0; i < filterLimit; i++) {
250 truncatedRawContactIds[i] = allRawContactIds[i];
251 }
252 return truncatedRawContactIds;
253 }
254 return allRawContactIds;
255 }
257 // "match" can only be used with the "tel" field
258 if ("match".equals(filterOp)) {
259 for (int i = 0; i < filterBy.length(); i++) {
260 if (!"tel".equals(filterBy.getString(i))) {
261 Log.w(LOGTAG, "\"match\" filterBy option is only valid for the \"tel\" field");
262 return null;
263 }
264 }
265 }
267 // Only select contacts from the selected account
268 String selection = null;
269 String[] selectionArgs = null;
271 if (mAccountName != null) {
272 selection = RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?";
273 selectionArgs = new String[] {mAccountName, mAccountType};
274 }
277 final String[] columnsToGet;
279 // If a filterBy value was not specified, search all columns
280 if (filterBy == null || filterBy.length() == 0) {
281 columnsToGet = null;
282 } else {
283 // Only get the columns given in the filterBy array
284 List<String> columnsToGetList = new ArrayList<String>();
286 columnsToGetList.add(Data.RAW_CONTACT_ID);
287 columnsToGetList.add(Data.MIMETYPE);
288 for (int i = 0; i < filterBy.length(); i++) {
289 final String field = filterBy.getString(i);
291 // If one of the filterBy fields is the ID, just return the filter value
292 // which should be the ID
293 if ("id".equals(field)) {
294 try {
295 return new long[] {Long.valueOf(filterValue)};
296 } catch (NumberFormatException e) {
297 // If the ID couldn't be converted to a long, it's invalid data
298 // so return null for failure
299 return null;
300 }
301 }
303 final String columnName = getColumnNameConstant(field);
305 if (columnName != null) {
306 columnsToGetList.add(columnName);
307 } else {
308 Log.w(LOGTAG, "Unknown filter option: " + field);
309 }
310 }
312 columnsToGet = columnsToGetList.toArray(new String[columnsToGetList.size()]);
313 }
315 // Execute the query
316 cursor = mContentResolver.query(Data.CONTENT_URI, columnsToGet, selection,
317 selectionArgs, null);
319 if (cursor.getCount() > 0) {
320 cursor.moveToPosition(-1);
321 while (cursor.moveToNext()) {
322 String mimeType = cursor.getString(cursor.getColumnIndex(Data.MIMETYPE));
324 // Check if the current mimetype is one of the types to filter by
325 if (filterBy != null && filterBy.length() > 0) {
326 for (int i = 0; i < filterBy.length(); i++) {
327 String currentFilterBy = filterBy.getString(i);
329 if (mimeType.equals(getMimeTypeOfField(currentFilterBy))) {
330 String columnName = getColumnNameConstant(currentFilterBy);
331 int columnIndex = cursor.getColumnIndex(columnName);
332 String databaseValue = cursor.getString(columnIndex);
334 boolean isPhone = false;
335 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
336 isPhone = true;
337 } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
338 // Translate the group ID to the group name for matching
339 try {
340 databaseValue = getGroupName(Long.valueOf(databaseValue));
341 } catch (NumberFormatException e) {
342 Log.e(LOGTAG, "Number Format Exception", e);
343 continue;
344 }
345 } else if (databaseValue == null) {
346 continue;
347 }
349 // Check if the value matches the filter value
350 if (isFindMatch(filterOp, filterValue, databaseValue, isPhone, substringMatching)) {
351 addMatchToList(cursor, rawContactIds);
352 break;
353 }
354 }
355 }
356 } else {
357 // If no filterBy options were given, check each column for a match
358 int numColumns = cursor.getColumnCount();
359 for (int i = 0; i < numColumns; i++) {
360 String databaseValue = cursor.getString(i);
361 if (databaseValue != null && isFindMatch(filterOp, filterValue, databaseValue, false, substringMatching)) {
362 addMatchToList(cursor, rawContactIds);
363 break;
364 }
365 }
366 }
368 // If the max found contacts size has been hit, stop looking for contacts
369 // A filter limit of 0 denotes there is no limit
370 if (filterLimit > 0 && filterLimit <= rawContactIds.size()) {
371 break;
372 }
373 }
374 }
375 } catch (JSONException e) {
376 throw new IllegalArgumentException(e);
377 } finally {
378 if (cursor != null) {
379 cursor.close();
380 }
381 }
383 // Return the contact IDs list converted to an array
384 return convertLongListToArray(rawContactIds);
385 }
387 private boolean isFindMatch(final String filterOp, String filterValue, String databaseValue,
388 final boolean isPhone, final int substringMatching) {
389 Log.i(LOGTAG, "matching: filterOp: " + filterOp);
390 if (DEBUG) {
391 Log.d(LOGTAG, "matching: filterValue: " + filterValue);
392 Log.d(LOGTAG, "matching: databaseValue: " + databaseValue);
393 }
394 Log.i(LOGTAG, "matching: isPhone: " + isPhone);
395 Log.i(LOGTAG, "matching: substringMatching: " + substringMatching);
397 if (databaseValue == null) {
398 return false;
399 }
401 filterValue = filterValue.toLowerCase();
402 databaseValue = databaseValue.toLowerCase();
404 if ("match".equals(filterOp)) {
405 // If substring matching is a positive number, only pay attention to the last X characters
406 // of both the filter and database values
407 if (substringMatching > 0) {
408 databaseValue = substringStartFromEnd(cleanPhoneNumber(databaseValue), substringMatching);
409 filterValue = substringStartFromEnd(cleanPhoneNumber(filterValue), substringMatching);
410 return databaseValue.startsWith(filterValue);
411 }
412 return databaseValue.equals(filterValue);
413 } else if ("equals".equals(filterOp)) {
414 if (isPhone) {
415 return PhoneNumberUtils.compare(filterValue, databaseValue);
416 }
417 return databaseValue.equals(filterValue);
418 } else if ("contains".equals(filterOp)) {
419 if (isPhone) {
420 filterValue = cleanPhoneNumber(filterValue);
421 databaseValue = cleanPhoneNumber(databaseValue);
422 }
423 return databaseValue.contains(filterValue);
424 } else if ("startsWith".equals(filterOp)) {
425 // If a phone number, remove non-dialable characters and then only pay attention to
426 // the last X digits given by the substring matching values (see bug 877302)
427 if (isPhone) {
428 String cleanedDatabasePhone = cleanPhoneNumber(databaseValue);
429 if (substringMatching > 0) {
430 cleanedDatabasePhone = substringStartFromEnd(cleanedDatabasePhone, substringMatching);
431 }
433 if (cleanedDatabasePhone.startsWith(filterValue)) {
434 return true;
435 }
436 }
437 return databaseValue.startsWith(filterValue);
438 }
439 return false;
440 }
442 private static String cleanPhoneNumber(String phone) {
443 return phone.replace(" ", "").replace("(", "").replace(")", "").replace("-", "");
444 }
446 private static String substringStartFromEnd(final String string, final int distanceFromEnd) {
447 int stringLen = string.length();
448 if (stringLen < distanceFromEnd) {
449 return string;
450 }
451 return string.substring(stringLen - distanceFromEnd);
452 }
454 private static void addMatchToList(final Cursor cursor, List<Long> rawContactIds) {
455 long rawContactId = cursor.getLong(cursor.getColumnIndex(Data.RAW_CONTACT_ID));
456 if (!rawContactIds.contains(rawContactId)) {
457 rawContactIds.add(rawContactId);
458 }
459 }
461 private JSONArray getContactsAsJSONArray(final long[] rawContactIds, final String sortBy, final String sortOrder) {
462 List<JSONObject> contactsList = new ArrayList<JSONObject>();
463 JSONArray contactsArray = new JSONArray();
465 // Get each contact as a JSON object
466 for (int i = 0; i < rawContactIds.length; i++) {
467 contactsList.add(getContactAsJSONObject(rawContactIds[i]));
468 }
470 // Sort the contacts
471 if (sortBy != null) {
472 Collections.sort(contactsList, new ContactsComparator(sortBy, sortOrder));
473 }
475 // Convert the contacts list to a JSON array
476 for (int i = 0; i < contactsList.size(); i++) {
477 contactsArray.put(contactsList.get(i));
478 }
480 return contactsArray;
481 }
483 private JSONObject getContactAsJSONObject(long rawContactId) {
484 // ContactManager wants a contact object with it's properties wrapped in an array of objects
485 JSONObject contact = new JSONObject();
486 JSONObject contactProperties = new JSONObject();
488 JSONArray names = new JSONArray();
489 JSONArray givenNames = new JSONArray();
490 JSONArray familyNames = new JSONArray();
491 JSONArray honorificPrefixes = new JSONArray();
492 JSONArray honorificSuffixes = new JSONArray();
493 JSONArray additionalNames = new JSONArray();
494 JSONArray nicknames = new JSONArray();
495 JSONArray addresses = new JSONArray();
496 JSONArray phones = new JSONArray();
497 JSONArray emails = new JSONArray();
498 JSONArray organizations = new JSONArray();
499 JSONArray jobTitles = new JSONArray();
500 JSONArray notes = new JSONArray();
501 JSONArray urls = new JSONArray();
502 JSONArray impps = new JSONArray();
503 JSONArray categories = new JSONArray();
504 String bday = null;
505 String anniversary = null;
506 String sex = null;
507 String genderIdentity = null;
508 JSONArray key = new JSONArray();
510 // Get all the data columns
511 final String[] columnsToGet = getAllColumns();
513 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
514 Uri entityUri = Uri.withAppendedPath(rawContactUri, Entity.CONTENT_DIRECTORY);
516 Cursor cursor = mContentResolver.query(entityUri, columnsToGet, null, null, null);
517 cursor.moveToPosition(-1);
518 while (cursor.moveToNext()) {
519 String mimeType = cursor.getString(cursor.getColumnIndex(Data.MIMETYPE));
521 // Put the proper fields for each mimetype into the JSON arrays
522 try {
523 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
524 final String displayName = cursor.getString(cursor.getColumnIndex(StructuredName.DISPLAY_NAME));
525 final String givenName = cursor.getString(cursor.getColumnIndex(StructuredName.GIVEN_NAME));
526 final String familyName = cursor.getString(cursor.getColumnIndex(StructuredName.FAMILY_NAME));
527 final String prefix = cursor.getString(cursor.getColumnIndex(StructuredName.PREFIX));
528 final String suffix = cursor.getString(cursor.getColumnIndex(StructuredName.SUFFIX));
530 if (displayName != null) {
531 names.put(displayName);
532 }
533 if (givenName != null) {
534 givenNames.put(givenName);
535 }
536 if (familyName != null) {
537 familyNames.put(familyName);
538 }
539 if (prefix != null) {
540 honorificPrefixes.put(prefix);
541 }
542 if (suffix != null) {
543 honorificSuffixes.put(suffix);
544 }
546 } else if (MIMETYPE_ADDITIONAL_NAME.equals(mimeType)) {
547 additionalNames.put(cursor.getString(cursor.getColumnIndex(CUSTOM_DATA_COLUMN)));
549 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
550 nicknames.put(cursor.getString(cursor.getColumnIndex(Nickname.NAME)));
552 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
553 initAddressTypesMap();
554 getAddressDataAsJSONObject(cursor, addresses);
556 } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
557 initPhoneTypesMap();
558 getPhoneDataAsJSONObject(cursor, phones);
560 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
561 initEmailTypesMap();
562 getGenericDataAsJSONObject(cursor, emails, Email.ADDRESS, Email.TYPE, Email.LABEL, mEmailTypesMap);
564 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
565 getOrganizationDataAsJSONObject(cursor, organizations, jobTitles);
567 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
568 notes.put(cursor.getString(cursor.getColumnIndex(Note.NOTE)));
570 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
571 initWebsiteTypesMap();
572 getGenericDataAsJSONObject(cursor, urls, Website.URL, Website.TYPE, Website.LABEL, mWebsiteTypesMap);
574 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
575 initImTypesMap();
576 getGenericDataAsJSONObject(cursor, impps, Im.DATA, Im.TYPE, Im.LABEL, mImTypesMap);
578 } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
579 long groupId = cursor.getLong(cursor.getColumnIndex(GroupMembership.GROUP_ROW_ID));
580 String groupName = getGroupName(groupId);
581 if (!doesJSONArrayContainString(categories, groupName)) {
582 categories.put(groupName);
583 }
585 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
586 int type = cursor.getInt(cursor.getColumnIndex(Event.TYPE));
587 String date = cursor.getString(cursor.getColumnIndex(Event.START_DATE));
589 // Add the time info onto the date so it correctly parses into a JS date object
590 date += "T00:00:00";
592 switch (type) {
593 case Event.TYPE_BIRTHDAY:
594 bday = date;
595 break;
597 case Event.TYPE_ANNIVERSARY:
598 anniversary = date;
599 break;
600 }
602 } else if (MIMETYPE_SEX.equals(mimeType)) {
603 sex = cursor.getString(cursor.getColumnIndex(CUSTOM_DATA_COLUMN));
605 } else if (MIMETYPE_GENDER_IDENTITY.equals(mimeType)) {
606 genderIdentity = cursor.getString(cursor.getColumnIndex(CUSTOM_DATA_COLUMN));
608 } else if (MIMETYPE_KEY.equals(mimeType)) {
609 key.put(cursor.getString(cursor.getColumnIndex(CUSTOM_DATA_COLUMN)));
610 }
611 } catch (JSONException e) {
612 throw new IllegalArgumentException(e);
613 }
614 }
615 cursor.close();
617 try {
618 // Add the fields to the contact properties object
619 contactProperties.put("name", names);
620 contactProperties.put("givenName", givenNames);
621 contactProperties.put("familyName", familyNames);
622 contactProperties.put("honorificPrefix", honorificPrefixes);
623 contactProperties.put("honorificSuffix", honorificSuffixes);
624 contactProperties.put("additionalName", additionalNames);
625 contactProperties.put("nickname", nicknames);
626 contactProperties.put("adr", addresses);
627 contactProperties.put("tel", phones);
628 contactProperties.put("email", emails);
629 contactProperties.put("org", organizations);
630 contactProperties.put("jobTitle", jobTitles);
631 contactProperties.put("note", notes);
632 contactProperties.put("url", urls);
633 contactProperties.put("impp", impps);
634 contactProperties.put("category", categories);
635 contactProperties.put("key", key);
637 putPossibleNullValueInJSONObject("bday", bday, contactProperties);
638 putPossibleNullValueInJSONObject("anniversary", anniversary, contactProperties);
639 putPossibleNullValueInJSONObject("sex", sex, contactProperties);
640 putPossibleNullValueInJSONObject("genderIdentity", genderIdentity, contactProperties);
642 // Add the raw contact ID and the properties to the contact
643 contact.put("id", String.valueOf(rawContactId));
644 contact.put("updated", null);
645 contact.put("published", null);
646 contact.put("properties", contactProperties);
647 } catch (JSONException e) {
648 throw new IllegalArgumentException(e);
649 }
651 if (DEBUG) {
652 try {
653 Log.d(LOGTAG, "Got contact: " + contact.toString(3));
654 } catch (JSONException e) {}
655 }
657 return contact;
658 }
660 private boolean bool(int integer) {
661 return integer != 0 ? true : false;
662 }
664 private void getGenericDataAsJSONObject(Cursor cursor, JSONArray array, final String dataColumn,
665 final String typeColumn, final String typeLabelColumn,
666 final HashMap<String, Integer> typeMap) throws JSONException {
667 String value = cursor.getString(cursor.getColumnIndex(dataColumn));
668 int typeConstant = cursor.getInt(cursor.getColumnIndex(typeColumn));
669 String type;
670 if (typeConstant == BaseTypes.TYPE_CUSTOM) {
671 type = cursor.getString(cursor.getColumnIndex(typeLabelColumn));
672 } else {
673 type = getKeyFromMapValue(typeMap, Integer.valueOf(typeConstant));
674 }
676 // Since an object may have multiple types, it may have already been added,
677 // but still needs the new type added
678 boolean found = false;
679 if (type != null) {
680 for (int i = 0; i < array.length(); i++) {
681 JSONObject object = array.getJSONObject(i);
682 if (value.equals(object.getString("value"))) {
683 found = true;
685 JSONArray types = object.getJSONArray("type");
686 if (!doesJSONArrayContainString(types, type)) {
687 types.put(type);
688 break;
689 }
690 }
691 }
692 }
694 // If an existing object wasn't found, make a new one
695 if (!found) {
696 JSONObject object = new JSONObject();
697 JSONArray types = new JSONArray();
698 object.put("value", value);
699 types.put(type);
700 object.put("type", types);
701 object.put("pref", bool(cursor.getInt(cursor.getColumnIndex(Data.IS_SUPER_PRIMARY))));
703 array.put(object);
704 }
705 }
707 private void getPhoneDataAsJSONObject(Cursor cursor, JSONArray phones) throws JSONException {
708 String value = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
709 int typeConstant = cursor.getInt(cursor.getColumnIndex(Phone.TYPE));
710 String type;
711 if (typeConstant == Phone.TYPE_CUSTOM) {
712 type = cursor.getString(cursor.getColumnIndex(Phone.LABEL));
713 } else {
714 type = getKeyFromMapValue(mPhoneTypesMap, Integer.valueOf(typeConstant));
715 }
717 // Since a phone may have multiple types, it may have already been added,
718 // but still needs the new type added
719 boolean found = false;
720 if (type != null) {
721 for (int i = 0; i < phones.length(); i++) {
722 JSONObject phone = phones.getJSONObject(i);
723 if (value.equals(phone.getString("value"))) {
724 found = true;
726 JSONArray types = phone.getJSONArray("type");
727 if (!doesJSONArrayContainString(types, type)) {
728 types.put(type);
729 break;
730 }
731 }
732 }
733 }
735 // If an existing phone wasn't found, make a new one
736 if (!found) {
737 JSONObject phone = new JSONObject();
738 JSONArray types = new JSONArray();
739 phone.put("value", value);
740 phone.put("type", type);
741 types.put(type);
742 phone.put("type", types);
743 phone.put("carrier", cursor.getString(cursor.getColumnIndex(CARRIER_COLUMN)));
744 phone.put("pref", bool(cursor.getInt(cursor.getColumnIndex(Phone.IS_SUPER_PRIMARY))));
746 phones.put(phone);
747 }
748 }
750 private void getAddressDataAsJSONObject(Cursor cursor, JSONArray addresses) throws JSONException {
751 String streetAddress = cursor.getString(cursor.getColumnIndex(StructuredPostal.STREET));
752 String locality = cursor.getString(cursor.getColumnIndex(StructuredPostal.CITY));
753 String region = cursor.getString(cursor.getColumnIndex(StructuredPostal.REGION));
754 String postalCode = cursor.getString(cursor.getColumnIndex(StructuredPostal.POSTCODE));
755 String countryName = cursor.getString(cursor.getColumnIndex(StructuredPostal.COUNTRY));
756 int typeConstant = cursor.getInt(cursor.getColumnIndex(StructuredPostal.TYPE));
757 String type;
758 if (typeConstant == StructuredPostal.TYPE_CUSTOM) {
759 type = cursor.getString(cursor.getColumnIndex(StructuredPostal.LABEL));
760 } else {
761 type = getKeyFromMapValue(mAddressTypesMap, Integer.valueOf(typeConstant));
762 }
764 // Since an email may have multiple types, it may have already been added,
765 // but still needs the new type added
766 boolean found = false;
767 if (type != null) {
768 for (int i = 0; i < addresses.length(); i++) {
769 JSONObject address = addresses.getJSONObject(i);
770 if (streetAddress.equals(address.getString("streetAddress")) &&
771 locality.equals(address.getString("locality")) &&
772 region.equals(address.getString("region")) &&
773 countryName.equals(address.getString("countryName")) &&
774 postalCode.equals(address.getString("postalCode"))) {
775 found = true;
777 JSONArray types = address.getJSONArray("type");
778 if (!doesJSONArrayContainString(types, type)) {
779 types.put(type);
780 break;
781 }
782 }
783 }
784 }
786 // If an existing email wasn't found, make a new one
787 if (!found) {
788 JSONObject address = new JSONObject();
789 JSONArray types = new JSONArray();
790 address.put("streetAddress", streetAddress);
791 address.put("locality", locality);
792 address.put("region", region);
793 address.put("countryName", countryName);
794 address.put("postalCode", postalCode);
795 types.put(type);
796 address.put("type", types);
797 address.put("pref", bool(cursor.getInt(cursor.getColumnIndex(StructuredPostal.IS_SUPER_PRIMARY))));
799 addresses.put(address);
800 }
801 }
803 private void getOrganizationDataAsJSONObject(Cursor cursor, JSONArray organizations,
804 JSONArray jobTitles) throws JSONException {
805 int organizationColumnIndex = cursor.getColumnIndex(Organization.COMPANY);
806 int titleColumnIndex = cursor.getColumnIndex(Organization.TITLE);
808 if (!cursor.isNull(organizationColumnIndex)) {
809 organizations.put(cursor.getString(organizationColumnIndex));
810 }
811 if (!cursor.isNull(titleColumnIndex)) {
812 jobTitles.put(cursor.getString(titleColumnIndex));
813 }
814 }
816 private class ContactsComparator implements Comparator<JSONObject> {
817 final String mSortBy;
818 final String mSortOrder;
820 public ContactsComparator(final String sortBy, final String sortOrder) {
821 mSortBy = sortBy.toLowerCase();
822 mSortOrder = sortOrder.toLowerCase();
823 }
825 @Override
826 public int compare(JSONObject left, JSONObject right) {
827 // Determine if sorting by "family name, given name" or "given name, family name"
828 boolean familyFirst = false;
829 if ("familyname".equals(mSortBy)) {
830 familyFirst = true;
831 }
833 JSONObject leftProperties;
834 JSONObject rightProperties;
835 try {
836 leftProperties = left.getJSONObject("properties");
837 rightProperties = right.getJSONObject("properties");
838 } catch (JSONException e) {
839 throw new IllegalArgumentException(e);
840 }
842 JSONArray leftFamilyNames = leftProperties.optJSONArray("familyName");
843 JSONArray leftGivenNames = leftProperties.optJSONArray("givenName");
844 JSONArray rightFamilyNames = rightProperties.optJSONArray("familyName");
845 JSONArray rightGivenNames = rightProperties.optJSONArray("givenName");
847 // If any of the name arrays didn't exist (are null), create empty arrays
848 // to avoid doing a bunch of null checking below
849 if (leftFamilyNames == null) {
850 leftFamilyNames = new JSONArray();
851 }
852 if (leftGivenNames == null) {
853 leftGivenNames = new JSONArray();
854 }
855 if (rightFamilyNames == null) {
856 rightFamilyNames = new JSONArray();
857 }
858 if (rightGivenNames == null) {
859 rightGivenNames = new JSONArray();
860 }
862 int maxArrayLength = max(leftFamilyNames.length(), leftGivenNames.length(),
863 rightFamilyNames.length(), rightGivenNames.length());
865 int index = 0;
866 int compareResult;
867 do {
868 // Join together the given name and family name per the pattern above
869 String leftName = "";
870 String rightName = "";
872 if (familyFirst) {
873 leftName = leftFamilyNames.optString(index, "") + leftGivenNames.optString(index, "");
874 rightName = rightFamilyNames.optString(index, "") + rightGivenNames.optString(index, "");
875 } else {
876 leftName = leftGivenNames.optString(index, "") + leftFamilyNames.optString(index, "");
877 rightName = rightGivenNames.optString(index, "") + rightFamilyNames.optString(index, "");
878 }
880 index++;
881 compareResult = leftName.compareTo(rightName);
883 } while (compareResult == 0 && index < maxArrayLength);
885 // If descending order, flip the result
886 if (compareResult != 0 && "descending".equals(mSortOrder)) {
887 compareResult = -compareResult;
888 }
890 return compareResult;
891 }
892 }
894 private void clearAllContacts(final JSONObject contactOptions, final String requestID) {
895 ArrayList<ContentProviderOperation> deleteOptions = new ArrayList<ContentProviderOperation>();
897 // Delete all contacts from the selected account
898 ContentProviderOperation.Builder deleteOptionsBuilder = ContentProviderOperation.newDelete(RawContacts.CONTENT_URI);
899 if (mAccountName != null) {
900 deleteOptionsBuilder.withSelection(RawContacts.ACCOUNT_NAME + "=?", new String[] {mAccountName})
901 .withSelection(RawContacts.ACCOUNT_TYPE + "=?", new String[] {mAccountType});
902 }
904 deleteOptions.add(deleteOptionsBuilder.build());
906 // Clear the contacts
907 String returnStatus = "KO";
908 if (applyBatch(deleteOptions) != null) {
909 returnStatus = "OK";
910 }
912 Log.i(LOGTAG, "Sending return status: " + returnStatus);
914 sendCallbackToJavascript("Android:Contacts:Clear:Return:" + returnStatus, requestID,
915 new String[] {"contactID"}, new Object[] {"undefined"});
917 }
919 private boolean deleteContact(String rawContactId) {
920 ContentProviderOperation deleteOptions = ContentProviderOperation.newDelete(RawContacts.CONTENT_URI)
921 .withSelection(RawContacts._ID + "=?",
922 new String[] {rawContactId})
923 .build();
925 ArrayList<ContentProviderOperation> deleteOptionsList = new ArrayList<ContentProviderOperation>();
926 deleteOptionsList.add(deleteOptions);
928 return checkForPositiveCountInResults(applyBatch(deleteOptionsList));
929 }
931 private void removeContact(final JSONObject contactOptions, final String requestID) {
932 String rawContactId;
933 try {
934 rawContactId = contactOptions.getString("id");
935 Log.i(LOGTAG, "Removing contact with ID: " + rawContactId);
936 } catch (JSONException e) {
937 // We can't continue without a raw contact ID
938 sendCallbackToJavascript("Android:Contact:Remove:Return:KO", requestID, null, null);
939 return;
940 }
942 String returnStatus = "KO";
943 if(deleteContact(rawContactId)) {
944 returnStatus = "OK";
945 }
947 sendCallbackToJavascript("Android:Contact:Remove:Return:" + returnStatus, requestID,
948 new String[] {"contactID"}, new Object[] {rawContactId});
949 }
951 private void saveContact(final JSONObject contactOptions, final String requestID) {
952 try {
953 String reason = contactOptions.getString("reason");
954 JSONObject contact = contactOptions.getJSONObject("contact");
955 JSONObject contactProperties = contact.getJSONObject("properties");
957 if ("update".equals(reason)) {
958 updateContact(contactProperties, contact.getLong("id"), requestID);
959 } else {
960 insertContact(contactProperties, requestID);
961 }
962 } catch (JSONException e) {
963 throw new IllegalArgumentException(e);
964 }
965 }
967 private void insertContact(final JSONObject contactProperties, final String requestID) throws JSONException {
968 ArrayList<ContentProviderOperation> newContactOptions = new ArrayList<ContentProviderOperation>();
970 // Account to save the contact under
971 newContactOptions.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
972 .withValue(RawContacts.ACCOUNT_NAME, mAccountName)
973 .withValue(RawContacts.ACCOUNT_TYPE, mAccountType)
974 .build());
976 List<ContentValues> newContactValues = getContactValues(contactProperties);
978 for (ContentValues values : newContactValues) {
979 newContactOptions.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
980 .withValueBackReference(Data.RAW_CONTACT_ID, 0)
981 .withValues(values)
982 .build());
983 }
985 String returnStatus = "KO";
986 Long newRawContactId = new Long(-1);
988 // Insert the contact!
989 ContentProviderResult[] insertResults = applyBatch(newContactOptions);
991 if (insertResults != null) {
992 try {
993 // Get the ID of the newly created contact
994 newRawContactId = getRawContactIdFromContentProviderResults(insertResults);
996 if (newRawContactId != null) {
997 returnStatus = "OK";
998 }
999 } catch (NumberFormatException e) {
1000 Log.e(LOGTAG, "NumberFormatException", e);
1001 }
1003 Log.i(LOGTAG, "Newly created contact ID: " + newRawContactId);
1004 }
1006 Log.i(LOGTAG, "Sending return status: " + returnStatus);
1008 sendCallbackToJavascript("Android:Contact:Save:Return:" + returnStatus, requestID,
1009 new String[] {"contactID", "reason"},
1010 new Object[] {newRawContactId, "create"});
1011 }
1013 private void updateContact(final JSONObject contactProperties, final long rawContactId, final String requestID) throws JSONException {
1014 // Why is updating a contact so weird and horribly inefficient? Because Android doesn't
1015 // like multiple values for contact fields, but the Mozilla contacts API calls for this.
1016 // This means the Android update function is essentially completely useless. Why not just
1017 // delete the contact and re-insert it? Because that would change the contact ID and the
1018 // Mozilla contacts API shouldn't have this behavior. The solution is to delete each
1019 // row from the contacts data table that belongs to the contact, and insert the new
1020 // fields. But then why not just delete all the data from the data in one go and
1021 // insert the new data in another? Because if all the data relating to a contact is
1022 // deleted, Android will "conviently" remove the ID making it impossible to insert data
1023 // under the old ID. To work around this, we put a Mozilla contact flag in the database
1025 ContentProviderOperation removeOptions = ContentProviderOperation.newDelete(Data.CONTENT_URI)
1026 .withSelection(Data.RAW_CONTACT_ID + "=? AND " +
1027 Data.MIMETYPE + " != '" + MIMETYPE_MOZILLA_CONTACTS_FLAG + "'",
1028 new String[] {String.valueOf(rawContactId)})
1029 .build();
1031 ArrayList<ContentProviderOperation> removeOptionsList = new ArrayList<ContentProviderOperation>();
1032 removeOptionsList.add(removeOptions);
1034 ContentProviderResult[] removeResults = applyBatch(removeOptionsList);
1036 // Check if the remove failed
1037 if (removeResults == null || !checkForPositiveCountInResults(removeResults)) {
1038 Log.w(LOGTAG, "Null or 0 remove results");
1040 sendCallbackToJavascript("Android:Contact:Save:Return:KO", requestID, null, null);
1041 return;
1042 }
1044 List<ContentValues> updateContactValues = getContactValues(contactProperties);
1045 ArrayList<ContentProviderOperation> updateContactOptions = new ArrayList<ContentProviderOperation>();
1047 for (ContentValues values : updateContactValues) {
1048 updateContactOptions.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
1049 .withValue(Data.RAW_CONTACT_ID, rawContactId)
1050 .withValues(values)
1051 .build());
1052 }
1054 String returnStatus = "KO";
1056 // Update the contact!
1057 applyBatch(updateContactOptions);
1059 sendCallbackToJavascript("Android:Contact:Save:Return:OK", requestID,
1060 new String[] {"contactID", "reason"},
1061 new Object[] {rawContactId, "update"});
1062 }
1064 private List<ContentValues> getContactValues(final JSONObject contactProperties) throws JSONException {
1065 List<ContentValues> contactValues = new ArrayList<ContentValues>();
1067 // Add the contact to the default group so it is shown in other apps
1068 // like the Contacts or People app
1069 ContentValues defaultGroupValues = new ContentValues();
1070 defaultGroupValues.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1071 defaultGroupValues.put(GroupMembership.GROUP_ROW_ID, mGroupId);
1072 contactValues.add(defaultGroupValues);
1074 // Create all the values that will be inserted into the new contact
1075 getNameValues(contactProperties.optJSONArray("name"),
1076 contactProperties.optJSONArray("givenName"),
1077 contactProperties.optJSONArray("familyName"),
1078 contactProperties.optJSONArray("honorificPrefix"),
1079 contactProperties.optJSONArray("honorificSuffix"),
1080 contactValues);
1082 getGenericValues(MIMETYPE_ADDITIONAL_NAME, CUSTOM_DATA_COLUMN,
1083 contactProperties.optJSONArray("additionalName"), contactValues);
1085 getNicknamesValues(contactProperties.optJSONArray("nickname"), contactValues);
1087 getAddressesValues(contactProperties.optJSONArray("adr"), contactValues);
1089 getPhonesValues(contactProperties.optJSONArray("tel"), contactValues);
1091 getEmailsValues(contactProperties.optJSONArray("email"), contactValues);
1093 //getPhotosValues(contactProperties.optJSONArray("photo"), contactValues);
1095 getGenericValues(Organization.CONTENT_ITEM_TYPE, Organization.COMPANY,
1096 contactProperties.optJSONArray("org"), contactValues);
1098 getGenericValues(Organization.CONTENT_ITEM_TYPE, Organization.TITLE,
1099 contactProperties.optJSONArray("jobTitle"), contactValues);
1101 getNotesValues(contactProperties.optJSONArray("note"), contactValues);
1103 getWebsitesValues(contactProperties.optJSONArray("url"), contactValues);
1105 getImsValues(contactProperties.optJSONArray("impp"), contactValues);
1107 getCategoriesValues(contactProperties.optJSONArray("category"), contactValues);
1109 getEventValues(contactProperties.optString("bday"), Event.TYPE_BIRTHDAY, contactValues);
1111 getEventValues(contactProperties.optString("anniversary"), Event.TYPE_ANNIVERSARY, contactValues);
1113 getCustomMimetypeValues(contactProperties.optString("sex"), MIMETYPE_SEX, contactValues);
1115 getCustomMimetypeValues(contactProperties.optString("genderIdentity"), MIMETYPE_GENDER_IDENTITY, contactValues);
1117 getGenericValues(MIMETYPE_KEY, CUSTOM_DATA_COLUMN, contactProperties.optJSONArray("key"),
1118 contactValues);
1120 return contactValues;
1121 }
1123 private void getGenericValues(final String mimeType, final String dataType, final JSONArray fields,
1124 List<ContentValues> newContactValues) throws JSONException {
1125 if (fields == null) {
1126 return;
1127 }
1129 for (int i = 0; i < fields.length(); i++) {
1130 ContentValues contentValues = new ContentValues();
1131 contentValues.put(Data.MIMETYPE, mimeType);
1132 contentValues.put(dataType, fields.getString(i));
1133 newContactValues.add(contentValues);
1134 }
1135 }
1137 private void getNameValues(final JSONArray displayNames, final JSONArray givenNames,
1138 final JSONArray familyNames, final JSONArray prefixes,
1139 final JSONArray suffixes, List<ContentValues> newContactValues) throws JSONException {
1140 int maxLen = max((displayNames != null ? displayNames.length() : 0),
1141 (givenNames != null ? givenNames.length() : 0),
1142 (familyNames != null ? familyNames.length() : 0),
1143 (prefixes != null ? prefixes.length() : 0),
1144 (suffixes != null ? suffixes.length() : 0));
1146 for (int i = 0; i < maxLen; i++) {
1147 ContentValues contentValues = new ContentValues();
1148 contentValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
1150 final String displayName = (displayNames != null ? displayNames.optString(i, null) : null);
1151 final String givenName = (givenNames != null ? givenNames.optString(i, null) : null);
1152 final String familyName = (familyNames != null ? familyNames.optString(i, null) : null);
1153 final String prefix = (prefixes != null ? prefixes.optString(i, null) : null);
1154 final String suffix = (suffixes != null ? suffixes.optString(i, null) : null);
1156 if (displayName != null) {
1157 contentValues.put(StructuredName.DISPLAY_NAME, displayName);
1158 }
1159 if (givenName != null) {
1160 contentValues.put(StructuredName.GIVEN_NAME, givenName);
1161 }
1162 if (familyName != null) {
1163 contentValues.put(StructuredName.FAMILY_NAME, familyName);
1164 }
1165 if (prefix != null) {
1166 contentValues.put(StructuredName.PREFIX, prefix);
1167 }
1168 if (suffix != null) {
1169 contentValues.put(StructuredName.SUFFIX, suffix);
1170 }
1172 newContactValues.add(contentValues);
1173 }
1174 }
1176 private void getNicknamesValues(final JSONArray nicknames, List<ContentValues> newContactValues) throws JSONException {
1177 if (nicknames == null) {
1178 return;
1179 }
1181 for (int i = 0; i < nicknames.length(); i++) {
1182 ContentValues contentValues = new ContentValues();
1183 contentValues.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
1184 contentValues.put(Nickname.NAME, nicknames.getString(i));
1185 contentValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1186 newContactValues.add(contentValues);
1187 }
1188 }
1190 private void getAddressesValues(final JSONArray addresses, List<ContentValues> newContactValues) throws JSONException {
1191 if (addresses == null) {
1192 return;
1193 }
1195 for (int i = 0; i < addresses.length(); i++) {
1196 JSONObject address = addresses.getJSONObject(i);
1197 JSONArray addressTypes = address.optJSONArray("type");
1199 if (addressTypes != null) {
1200 for (int j = 0; j < addressTypes.length(); j++) {
1201 // Translate the address type string to an integer constant
1202 // provided by the ContactsContract API
1203 final String type = addressTypes.getString(j);
1204 final int typeConstant = getAddressType(type);
1206 newContactValues.add(createAddressContentValues(address, typeConstant, type));
1207 }
1208 } else {
1209 newContactValues.add(createAddressContentValues(address, -1, null));
1210 }
1211 }
1212 }
1214 private ContentValues createAddressContentValues(final JSONObject address, final int typeConstant,
1215 final String type) throws JSONException {
1216 ContentValues contentValues = new ContentValues();
1217 contentValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
1218 contentValues.put(StructuredPostal.STREET, address.optString("streetAddress"));
1219 contentValues.put(StructuredPostal.CITY, address.optString("locality"));
1220 contentValues.put(StructuredPostal.REGION, address.optString("region"));
1221 contentValues.put(StructuredPostal.POSTCODE, address.optString("postalCode"));
1222 contentValues.put(StructuredPostal.COUNTRY, address.optString("countryName"));
1224 if (type != null) {
1225 contentValues.put(StructuredPostal.TYPE, typeConstant);
1227 // If a custom type, add a label
1228 if (typeConstant == BaseTypes.TYPE_CUSTOM) {
1229 contentValues.put(StructuredPostal.LABEL, type);
1230 }
1231 }
1233 if (address.has("pref")) {
1234 contentValues.put(Data.IS_SUPER_PRIMARY, address.getBoolean("pref") ? 1 : 0);
1235 }
1237 return contentValues;
1238 }
1240 private void getPhonesValues(final JSONArray phones, List<ContentValues> newContactValues) throws JSONException {
1241 if (phones == null) {
1242 return;
1243 }
1245 for (int i = 0; i < phones.length(); i++) {
1246 JSONObject phone = phones.getJSONObject(i);
1247 JSONArray phoneTypes = phone.optJSONArray("type");
1248 ContentValues contentValues;
1250 if (phoneTypes != null && phoneTypes.length() > 0) {
1251 for (int j = 0; j < phoneTypes.length(); j++) {
1252 // Translate the phone type string to an integer constant
1253 // provided by the ContactsContract API
1254 final String type = phoneTypes.getString(j);
1255 final int typeConstant = getPhoneType(type);
1257 contentValues = createContentValues(Phone.CONTENT_ITEM_TYPE, phone.optString("value"),
1258 typeConstant, type, phone.optBoolean("pref"));
1259 if (phone.has("carrier")) {
1260 contentValues.put(CARRIER_COLUMN, phone.optString("carrier"));
1261 }
1262 newContactValues.add(contentValues);
1263 }
1264 } else {
1265 contentValues = createContentValues(Phone.CONTENT_ITEM_TYPE, phone.optString("value"),
1266 -1, null, phone.optBoolean("pref"));
1267 if (phone.has("carrier")) {
1268 contentValues.put(CARRIER_COLUMN, phone.optString("carrier"));
1269 }
1270 newContactValues.add(contentValues);
1271 }
1272 }
1273 }
1275 private void getEmailsValues(final JSONArray emails, List<ContentValues> newContactValues) throws JSONException {
1276 if (emails == null) {
1277 return;
1278 }
1280 for (int i = 0; i < emails.length(); i++) {
1281 JSONObject email = emails.getJSONObject(i);
1282 JSONArray emailTypes = email.optJSONArray("type");
1284 if (emailTypes != null && emailTypes.length() > 0) {
1285 for (int j = 0; j < emailTypes.length(); j++) {
1286 // Translate the email type string to an integer constant
1287 // provided by the ContactsContract API
1288 final String type = emailTypes.getString(j);
1289 final int typeConstant = getEmailType(type);
1291 newContactValues.add(createContentValues(Email.CONTENT_ITEM_TYPE,
1292 email.optString("value"),
1293 typeConstant, type,
1294 email.optBoolean("pref")));
1295 }
1296 } else {
1297 newContactValues.add(createContentValues(Email.CONTENT_ITEM_TYPE,
1298 email.optString("value"),
1299 -1, null, email.optBoolean("pref")));
1300 }
1301 }
1302 }
1304 private void getPhotosValues(final JSONArray photos, List<ContentValues> newContactValues) throws JSONException {
1305 if (photos == null) {
1306 return;
1307 }
1309 // TODO: implement this
1310 }
1312 private void getNotesValues(final JSONArray notes, List<ContentValues> newContactValues) throws JSONException {
1313 if (notes == null) {
1314 return;
1315 }
1317 for (int i = 0; i < notes.length(); i++) {
1318 ContentValues contentValues = new ContentValues();
1319 contentValues.put(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
1320 contentValues.put(Note.NOTE, notes.getString(i));
1321 newContactValues.add(contentValues);
1322 }
1323 }
1325 private void getWebsitesValues(final JSONArray websites, List<ContentValues> newContactValues) throws JSONException {
1326 if (websites == null) {
1327 return;
1328 }
1330 for (int i = 0; i < websites.length(); i++) {
1331 JSONObject website = websites.getJSONObject(i);
1332 JSONArray websiteTypes = website.optJSONArray("type");
1334 if (websiteTypes != null && websiteTypes.length() > 0) {
1335 for (int j = 0; j < websiteTypes.length(); j++) {
1336 // Translate the website type string to an integer constant
1337 // provided by the ContactsContract API
1338 final String type = websiteTypes.getString(j);
1339 final int typeConstant = getWebsiteType(type);
1341 newContactValues.add(createContentValues(Website.CONTENT_ITEM_TYPE,
1342 website.optString("value"),
1343 typeConstant, type,
1344 website.optBoolean("pref")));
1345 }
1346 } else {
1347 newContactValues.add(createContentValues(Website.CONTENT_ITEM_TYPE,
1348 website.optString("value"),
1349 -1, null, website.optBoolean("pref")));
1350 }
1351 }
1352 }
1354 private void getImsValues(final JSONArray ims, List<ContentValues> newContactValues) throws JSONException {
1355 if (ims == null) {
1356 return;
1357 }
1359 for (int i = 0; i < ims.length(); i++) {
1360 JSONObject im = ims.getJSONObject(i);
1361 JSONArray imTypes = im.optJSONArray("type");
1363 if (imTypes != null && imTypes.length() > 0) {
1364 for (int j = 0; j < imTypes.length(); j++) {
1365 // Translate the IM type string to an integer constant
1366 // provided by the ContactsContract API
1367 final String type = imTypes.getString(j);
1368 final int typeConstant = getImType(type);
1370 newContactValues.add(createContentValues(Im.CONTENT_ITEM_TYPE,
1371 im.optString("value"),
1372 typeConstant, type,
1373 im.optBoolean("pref")));
1374 }
1375 } else {
1376 newContactValues.add(createContentValues(Im.CONTENT_ITEM_TYPE,
1377 im.optString("value"),
1378 -1, null, im.optBoolean("pref")));
1379 }
1380 }
1381 }
1383 private void getCategoriesValues(final JSONArray categories, List<ContentValues> newContactValues) throws JSONException {
1384 if (categories == null) {
1385 return;
1386 }
1388 for (int i = 0; i < categories.length(); i++) {
1389 String category = categories.getString(i);
1391 if ("my contacts".equals(category.toLowerCase()) ||
1392 PRE_HONEYCOMB_DEFAULT_GROUP.equalsIgnoreCase(category)) {
1393 Log.w(LOGTAG, "New contacts are implicitly added to the default group.");
1394 continue;
1395 }
1397 // Find the group ID of the given category
1398 long groupId = getGroupId(category);
1400 // Create the group if it doesn't already exist
1401 if (groupId == -1) {
1402 groupId = createGroup(category);
1403 // If the group is still -1, we failed to create the group
1404 if (groupId == -1) {
1405 // Only log the category name if in debug
1406 if (DEBUG) {
1407 Log.d(LOGTAG, "Failed to create new group for category \"" + category + "\"");
1408 } else {
1409 Log.w(LOGTAG, "Failed to create new group for given category.");
1410 }
1411 continue;
1412 }
1413 }
1415 ContentValues contentValues = new ContentValues();
1416 contentValues.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1417 contentValues.put(GroupMembership.GROUP_ROW_ID, groupId);
1418 newContactValues.add(contentValues);
1420 newContactValues.add(contentValues);
1421 }
1422 }
1424 private void getEventValues(final String event, final int type, List<ContentValues> newContactValues) {
1425 if (event == null || event.length() < 11) {
1426 return;
1427 }
1429 ContentValues contentValues = new ContentValues();
1430 contentValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1431 contentValues.put(Event.START_DATE, event.substring(0, 10));
1432 contentValues.put(Event.TYPE, type);
1433 newContactValues.add(contentValues);
1434 }
1436 private void getCustomMimetypeValues(final String value, final String mimeType, List<ContentValues> newContactValues) {
1437 if (value == null || "null".equals(value)) {
1438 return;
1439 }
1441 ContentValues contentValues = new ContentValues();
1442 contentValues.put(Data.MIMETYPE, mimeType);
1443 contentValues.put(CUSTOM_DATA_COLUMN, value);
1444 newContactValues.add(contentValues);
1445 }
1447 private void getMozillaContactFlagValues(List<ContentValues> newContactValues) {
1448 try {
1449 JSONArray mozillaContactsFlag = new JSONArray();
1450 mozillaContactsFlag.put("1");
1451 getGenericValues(MIMETYPE_MOZILLA_CONTACTS_FLAG, CUSTOM_DATA_COLUMN, mozillaContactsFlag, newContactValues);
1452 } catch (JSONException e) {
1453 throw new IllegalArgumentException(e);
1454 }
1455 }
1457 private ContentValues createContentValues(final String mimeType, final String value, final int typeConstant,
1458 final String type, final boolean preferredValue) {
1459 ContentValues contentValues = new ContentValues();
1460 contentValues.put(Data.MIMETYPE, mimeType);
1461 contentValues.put(Data.DATA1, value);
1462 contentValues.put(Data.IS_SUPER_PRIMARY, preferredValue ? 1 : 0);
1464 if (type != null) {
1465 contentValues.put(Data.DATA2, typeConstant);
1467 // If a custom type, add a label
1468 if (typeConstant == BaseTypes.TYPE_CUSTOM) {
1469 contentValues.put(Data.DATA3, type);
1470 }
1471 }
1473 return contentValues;
1474 }
1476 private void getContactsCount(final String requestID) {
1477 Cursor cursor = getAllRawContactIdsCursor();
1478 Integer numContacts = Integer.valueOf(cursor.getCount());
1479 cursor.close();
1481 sendCallbackToJavascript("Android:Contacts:Count", requestID, new String[] {"count"},
1482 new Object[] {numContacts});
1483 }
1485 private void sendCallbackToJavascript(final String subject, final String requestID,
1486 final String[] argNames, final Object[] argValues) {
1487 // Check the same number of argument names and arguments were given
1488 if (argNames != null && argNames.length != argValues.length) {
1489 throw new IllegalArgumentException("Argument names and argument values lengths do not match. " +
1490 "Names length = " + argNames.length + ", Values length = " +
1491 argValues.length);
1492 }
1494 try {
1495 JSONObject callbackMessage = new JSONObject();
1496 callbackMessage.put("requestID", requestID);
1498 if (argNames != null) {
1499 for (int i = 0; i < argNames.length; i++) {
1500 callbackMessage.put(argNames[i], argValues[i]);
1501 }
1502 }
1504 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(subject, callbackMessage.toString()));
1505 } catch (JSONException e) {
1506 throw new IllegalArgumentException(e);
1507 }
1508 }
1510 private void registerEventListener(final String event) {
1511 mEventDispatcher.registerEventListener(event, this);
1512 }
1514 private void unregisterEventListener(final String event) {
1515 mEventDispatcher.unregisterEventListener(event, this);
1516 }
1518 private ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) {
1519 try {
1520 return mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
1521 } catch (RemoteException e) {
1522 Log.e(LOGTAG, "RemoteException", e);
1523 } catch (OperationApplicationException e) {
1524 Log.e(LOGTAG, "OperationApplicationException", e);
1525 }
1526 return null;
1527 }
1529 private void getDeviceAccount(final Runnable handleMessage) {
1530 Account[] accounts = AccountManager.get(mActivity).getAccounts();
1532 if (accounts.length == 0) {
1533 Log.w(LOGTAG, "No accounts available");
1534 gotDeviceAccount(handleMessage);
1535 } else if (accounts.length > 1) {
1536 // Show the accounts chooser dialog if more than one dialog exists
1537 showAccountsDialog(accounts, handleMessage);
1538 } else {
1539 // If only one account exists, use it
1540 mAccountName = accounts[0].name;
1541 mAccountType = accounts[0].type;
1542 gotDeviceAccount(handleMessage);
1543 }
1545 mGotDeviceAccount = true;
1546 }
1548 private void showAccountsDialog(final Account[] accounts, final Runnable handleMessage) {
1549 String[] accountNames = new String[accounts.length];
1550 for (int i = 0; i < accounts.length; i++) {
1551 accountNames[i] = accounts[i].name;
1552 }
1554 final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
1555 builder.setTitle(mActivity.getResources().getString(R.string.contacts_account_chooser_dialog_title))
1556 .setSingleChoiceItems(accountNames, 0, new DialogInterface.OnClickListener() {
1557 @Override
1558 public void onClick(DialogInterface dialog, int position) {
1559 // Set the account name and type when an item is selected and dismiss the dialog
1560 mAccountName = accounts[position].name;
1561 mAccountType = accounts[position].type;
1562 dialog.dismiss();
1563 gotDeviceAccount(handleMessage);
1564 }
1565 });
1567 mActivity.runOnUiThread(new Runnable() {
1568 public void run() {
1569 builder.show();
1570 }
1571 });
1572 }
1574 private void gotDeviceAccount(final Runnable handleMessage) {
1575 // Force the handleMessage runnable and getDefaultGroupId to run on the background thread
1576 Runnable runnable = new Runnable() {
1577 @Override
1578 public void run() {
1579 getDefaultGroupId();
1581 // Don't log a user's account if not debug mode. Otherwise, just log a message
1582 // saying that we got an account to use
1583 if (mAccountName == null) {
1584 Log.i(LOGTAG, "No device account selected. Leaving account as null.");
1585 } else if (DEBUG) {
1586 Log.d(LOGTAG, "Using account: " + mAccountName + " (type: " + mAccountType + ")");
1587 } else {
1588 Log.i(LOGTAG, "Got device account to use for contact operations.");
1589 }
1590 handleMessage.run();
1591 }
1592 };
1594 ThreadUtils.postToBackgroundThread(runnable);
1595 }
1597 private void getDefaultGroupId() {
1598 Cursor cursor = getAllGroups();
1600 cursor.moveToPosition(-1);
1601 while (cursor.moveToNext()) {
1602 // Check if the account name and type for the group match the account name and type of
1603 // the account we're working with
1604 final String groupAccountName = cursor.getString(GROUP_ACCOUNT_NAME);
1605 if (!groupAccountName.equals(mAccountName)) {
1606 continue;
1607 }
1609 final String groupAccountType = cursor.getString(GROUP_ACCOUNT_TYPE);
1610 if (!groupAccountType.equals(mAccountType)) {
1611 continue;
1612 }
1614 // For all honeycomb and up, the default group is the first one which has the AUTO_ADD flag set
1615 if (isAutoAddGroup(cursor)) {
1616 mGroupTitle = cursor.getString(GROUP_TITLE);
1617 mGroupId = cursor.getLong(GROUP_ID);
1618 break;
1619 } else if (PRE_HONEYCOMB_DEFAULT_GROUP.equals(cursor.getString(GROUP_TITLE))) {
1620 mGroupId = cursor.getLong(GROUP_ID);
1621 mGroupTitle = PRE_HONEYCOMB_DEFAULT_GROUP;
1622 break;
1623 }
1624 }
1625 cursor.close();
1627 if (mGroupId == 0) {
1628 Log.w(LOGTAG, "Default group ID not found. Newly created contacts will not belong to any groups.");
1629 } else if (DEBUG) {
1630 Log.i(LOGTAG, "Using group ID: " + mGroupId + " (" + mGroupTitle + ")");
1631 }
1632 }
1634 private static boolean isAutoAddGroup(Cursor cursor) {
1635 // For Honeycomb and up, the default group is the first one which has the AUTO_ADD flag set.
1636 // For everything below Honeycomb, use the default "System Group: My Contacts" group
1637 return (Build.VERSION.SDK_INT >= 11 && !cursor.isNull(GROUP_AUTO_ADD) &&
1638 cursor.getInt(GROUP_AUTO_ADD) != 0);
1639 }
1641 private long getGroupId(String groupName) {
1642 long groupId = -1;
1643 Cursor cursor = getGroups(Groups.TITLE + " = '" + groupName + "'");
1645 cursor.moveToPosition(-1);
1646 while (cursor.moveToNext()) {
1647 String groupAccountName = cursor.getString(GROUP_ACCOUNT_NAME);
1648 String groupAccountType = cursor.getString(GROUP_ACCOUNT_TYPE);
1650 // Check if the account name and type for the group match the account name and type of
1651 // the account we're working with or the default "Phone" account if no account was found
1652 if (groupAccountName.equals(mAccountName) && groupAccountType.equals(mAccountType) ||
1653 (mAccountName == null && "Phone".equals(groupAccountType))) {
1654 if (groupName.equals(cursor.getString(GROUP_TITLE))) {
1655 groupId = cursor.getLong(GROUP_ID);
1656 break;
1657 }
1658 }
1659 }
1660 cursor.close();
1662 return groupId;
1663 }
1665 private String getGroupName(long groupId) {
1666 Cursor cursor = getGroups(Groups._ID + " = " + groupId);
1668 if (cursor.getCount() == 0) {
1669 cursor.close();
1670 return null;
1671 }
1673 cursor.moveToPosition(0);
1674 String groupName = cursor.getString(cursor.getColumnIndex(Groups.TITLE));
1675 cursor.close();
1677 return groupName;
1678 }
1680 private Cursor getAllGroups() {
1681 return getGroups(null);
1682 }
1684 private Cursor getGroups(String selectArg) {
1685 String[] columns = new String[] {
1686 Groups.ACCOUNT_NAME,
1687 Groups.ACCOUNT_TYPE,
1688 Groups._ID,
1689 Groups.TITLE,
1690 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? Groups.AUTO_ADD : Groups._ID)
1691 };
1693 if (selectArg != null) {
1694 selectArg = "AND " + selectArg;
1695 } else {
1696 selectArg = "";
1697 }
1699 return mContentResolver.query(Groups.CONTENT_URI, columns,
1700 Groups.ACCOUNT_TYPE + " NOT NULL AND " +
1701 Groups.ACCOUNT_NAME + " NOT NULL " + selectArg, null, null);
1702 }
1704 private long createGroup(String groupName) {
1705 if (DEBUG) {
1706 Log.d(LOGTAG, "Creating group: " + groupName);
1707 }
1709 ArrayList<ContentProviderOperation> newGroupOptions = new ArrayList<ContentProviderOperation>();
1711 // Create the group under the account we're using
1712 // If no account is selected, use a default account name/type for the group
1713 newGroupOptions.add(ContentProviderOperation.newInsert(Groups.CONTENT_URI)
1714 .withValue(Groups.ACCOUNT_NAME, (mAccountName == null ? "Phone" : mAccountName))
1715 .withValue(Groups.ACCOUNT_TYPE, (mAccountType == null ? "Phone" : mAccountType))
1716 .withValue(Groups.TITLE, groupName)
1717 .withValue(Groups.GROUP_VISIBLE, true)
1718 .build());
1720 applyBatch(newGroupOptions);
1722 // Return the ID of the newly created group
1723 return getGroupId(groupName);
1724 }
1726 private long[] getAllRawContactIds() {
1727 Cursor cursor = getAllRawContactIdsCursor();
1729 // Put the ids into an array
1730 long[] ids = new long[cursor.getCount()];
1731 int index = 0;
1732 cursor.moveToPosition(-1);
1733 while(cursor.moveToNext()) {
1734 ids[index] = cursor.getLong(cursor.getColumnIndex(RawContacts._ID));
1735 index++;
1736 }
1737 cursor.close();
1739 return ids;
1740 }
1742 private Cursor getAllRawContactIdsCursor() {
1743 // When a contact is deleted, it actually just sets the deleted field to 1 until the
1744 // sync adapter actually deletes the contact later so ignore any contacts with the deleted
1745 // flag set
1746 String selection = RawContacts.DELETED + "=0";
1747 String[] selectionArgs = null;
1749 // Only get contacts from the selected account
1750 if (mAccountName != null) {
1751 selection += " AND " + RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?";
1752 selectionArgs = new String[] {mAccountName, mAccountType};
1753 }
1755 // Get the ID's of all contacts and use the number of contact ID's as
1756 // the total number of contacts
1757 return mContentResolver.query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID},
1758 selection, selectionArgs, null);
1759 }
1761 private static Long getRawContactIdFromContentProviderResults(ContentProviderResult[] results) throws NumberFormatException {
1762 for (int i = 0; i < results.length; i++) {
1763 if (results[i].uri == null) {
1764 continue;
1765 }
1767 String uri = results[i].uri.toString();
1768 // Check if the uri is from the raw contacts table
1769 if (uri.contains("raw_contacts")) {
1770 // The ID is the after the final forward slash in the URI
1771 return Long.parseLong(uri.substring(uri.lastIndexOf("/") + 1));
1772 }
1773 }
1775 return null;
1776 }
1778 private static boolean checkForPositiveCountInResults(ContentProviderResult[] results) {
1779 for (int i = 0; i < results.length; i++) {
1780 Integer count = results[i].count;
1782 if (DEBUG) {
1783 Log.d(LOGTAG, "Results count: " + count);
1784 }
1786 if (count != null && count > 0) {
1787 return true;
1788 }
1789 }
1791 return false;
1792 }
1794 private static long[] convertLongListToArray(List<Long> list) {
1795 long[] array = new long[list.size()];
1797 for (int i = 0; i < list.size(); i++) {
1798 array[i] = list.get(i);
1799 }
1801 return array;
1802 }
1804 private static boolean doesJSONArrayContainString(final JSONArray array, final String value) {
1805 for (int i = 0; i < array.length(); i++) {
1806 if (value.equals(array.optString(i))) {
1807 return true;
1808 }
1809 }
1811 return false;
1812 }
1814 private static int max(int... values) {
1815 int max = values[0];
1816 for (int value : values) {
1817 if (value > max) {
1818 max = value;
1819 }
1820 }
1821 return max;
1822 }
1824 private static void putPossibleNullValueInJSONObject(final String key, final Object value, JSONObject jsonObject) throws JSONException{
1825 if (value != null) {
1826 jsonObject.put(key, value);
1827 } else {
1828 jsonObject.put(key, JSONObject.NULL);
1829 }
1830 }
1832 private static String getKeyFromMapValue(final HashMap<String, Integer> map, Integer value) {
1833 for (Entry<String, Integer> entry : map.entrySet()) {
1834 if (value == entry.getValue()) {
1835 return entry.getKey();
1836 }
1837 }
1838 return null;
1839 }
1841 private String getColumnNameConstant(String field) {
1842 initColumnNameConstantsMap();
1843 return mColumnNameConstantsMap.get(field.toLowerCase());
1844 }
1846 private void initColumnNameConstantsMap() {
1847 if (mColumnNameConstantsMap != null) {
1848 return;
1849 }
1850 mColumnNameConstantsMap = new HashMap<String, String>();
1852 mColumnNameConstantsMap.put("name", StructuredName.DISPLAY_NAME);
1853 mColumnNameConstantsMap.put("givenname", StructuredName.GIVEN_NAME);
1854 mColumnNameConstantsMap.put("familyname", StructuredName.FAMILY_NAME);
1855 mColumnNameConstantsMap.put("honorificprefix", StructuredName.PREFIX);
1856 mColumnNameConstantsMap.put("honorificsuffix", StructuredName.SUFFIX);
1857 mColumnNameConstantsMap.put("additionalname", CUSTOM_DATA_COLUMN);
1858 mColumnNameConstantsMap.put("nickname", Nickname.NAME);
1859 mColumnNameConstantsMap.put("adr", StructuredPostal.STREET);
1860 mColumnNameConstantsMap.put("email", Email.ADDRESS);
1861 mColumnNameConstantsMap.put("url", Website.URL);
1862 mColumnNameConstantsMap.put("category", GroupMembership.GROUP_ROW_ID);
1863 mColumnNameConstantsMap.put("tel", Phone.NUMBER);
1864 mColumnNameConstantsMap.put("org", Organization.COMPANY);
1865 mColumnNameConstantsMap.put("jobTitle", Organization.TITLE);
1866 mColumnNameConstantsMap.put("note", Note.NOTE);
1867 mColumnNameConstantsMap.put("impp", Im.DATA);
1868 mColumnNameConstantsMap.put("sex", CUSTOM_DATA_COLUMN);
1869 mColumnNameConstantsMap.put("genderidentity", CUSTOM_DATA_COLUMN);
1870 mColumnNameConstantsMap.put("key", CUSTOM_DATA_COLUMN);
1871 }
1873 private String getMimeTypeOfField(String field) {
1874 initMimeTypeConstantsMap();
1875 return mMimeTypeConstantsMap.get(field.toLowerCase());
1876 }
1878 private void initMimeTypeConstantsMap() {
1879 if (mMimeTypeConstantsMap != null) {
1880 return;
1881 }
1882 mMimeTypeConstantsMap = new HashMap<String, String>();
1884 mMimeTypeConstantsMap.put("name", StructuredName.CONTENT_ITEM_TYPE);
1885 mMimeTypeConstantsMap.put("givenname", StructuredName.CONTENT_ITEM_TYPE);
1886 mMimeTypeConstantsMap.put("familyname", StructuredName.CONTENT_ITEM_TYPE);
1887 mMimeTypeConstantsMap.put("honorificprefix", StructuredName.CONTENT_ITEM_TYPE);
1888 mMimeTypeConstantsMap.put("honorificsuffix", StructuredName.CONTENT_ITEM_TYPE);
1889 mMimeTypeConstantsMap.put("additionalname", MIMETYPE_ADDITIONAL_NAME);
1890 mMimeTypeConstantsMap.put("nickname", Nickname.CONTENT_ITEM_TYPE);
1891 mMimeTypeConstantsMap.put("email", Email.CONTENT_ITEM_TYPE);
1892 mMimeTypeConstantsMap.put("url", Website.CONTENT_ITEM_TYPE);
1893 mMimeTypeConstantsMap.put("category", GroupMembership.CONTENT_ITEM_TYPE);
1894 mMimeTypeConstantsMap.put("tel", Phone.CONTENT_ITEM_TYPE);
1895 mMimeTypeConstantsMap.put("org", Organization.CONTENT_ITEM_TYPE);
1896 mMimeTypeConstantsMap.put("jobTitle", Organization.CONTENT_ITEM_TYPE);
1897 mMimeTypeConstantsMap.put("note", Note.CONTENT_ITEM_TYPE);
1898 mMimeTypeConstantsMap.put("impp", Im.CONTENT_ITEM_TYPE);
1899 mMimeTypeConstantsMap.put("sex", MIMETYPE_SEX);
1900 mMimeTypeConstantsMap.put("genderidentity", MIMETYPE_GENDER_IDENTITY);
1901 mMimeTypeConstantsMap.put("key", MIMETYPE_KEY);
1902 }
1904 private int getAddressType(String addressType) {
1905 initAddressTypesMap();
1906 Integer type = mAddressTypesMap.get(addressType.toLowerCase());
1907 return (type != null ? Integer.valueOf(type) : StructuredPostal.TYPE_CUSTOM);
1908 }
1910 private void initAddressTypesMap() {
1911 if (mAddressTypesMap != null) {
1912 return;
1913 }
1914 mAddressTypesMap = new HashMap<String, Integer>();
1916 mAddressTypesMap.put("home", StructuredPostal.TYPE_HOME);
1917 mAddressTypesMap.put("work", StructuredPostal.TYPE_WORK);
1918 }
1920 private int getPhoneType(String phoneType) {
1921 initPhoneTypesMap();
1922 Integer type = mPhoneTypesMap.get(phoneType.toLowerCase());
1923 return (type != null ? Integer.valueOf(type) : Phone.TYPE_CUSTOM);
1924 }
1926 private void initPhoneTypesMap() {
1927 if (mPhoneTypesMap != null) {
1928 return;
1929 }
1930 mPhoneTypesMap = new HashMap<String, Integer>();
1932 mPhoneTypesMap.put("home", Phone.TYPE_HOME);
1933 mPhoneTypesMap.put("mobile", Phone.TYPE_MOBILE);
1934 mPhoneTypesMap.put("work", Phone.TYPE_WORK);
1935 mPhoneTypesMap.put("fax home", Phone.TYPE_FAX_HOME);
1936 mPhoneTypesMap.put("fax work", Phone.TYPE_FAX_WORK);
1937 mPhoneTypesMap.put("pager", Phone.TYPE_PAGER);
1938 mPhoneTypesMap.put("callback", Phone.TYPE_CALLBACK);
1939 mPhoneTypesMap.put("car", Phone.TYPE_CAR);
1940 mPhoneTypesMap.put("company main", Phone.TYPE_COMPANY_MAIN);
1941 mPhoneTypesMap.put("isdn", Phone.TYPE_ISDN);
1942 mPhoneTypesMap.put("main", Phone.TYPE_MAIN);
1943 mPhoneTypesMap.put("fax other", Phone.TYPE_OTHER_FAX);
1944 mPhoneTypesMap.put("other fax", Phone.TYPE_OTHER_FAX);
1945 mPhoneTypesMap.put("radio", Phone.TYPE_RADIO);
1946 mPhoneTypesMap.put("telex", Phone.TYPE_TELEX);
1947 mPhoneTypesMap.put("tty", Phone.TYPE_TTY_TDD);
1948 mPhoneTypesMap.put("ttd", Phone.TYPE_TTY_TDD);
1949 mPhoneTypesMap.put("work mobile", Phone.TYPE_WORK_MOBILE);
1950 mPhoneTypesMap.put("work pager", Phone.TYPE_WORK_PAGER);
1951 mPhoneTypesMap.put("assistant", Phone.TYPE_ASSISTANT);
1952 mPhoneTypesMap.put("mms", Phone.TYPE_MMS);
1953 }
1955 private int getEmailType(String emailType) {
1956 initEmailTypesMap();
1957 Integer type = mEmailTypesMap.get(emailType.toLowerCase());
1958 return (type != null ? Integer.valueOf(type) : Email.TYPE_CUSTOM);
1959 }
1961 private void initEmailTypesMap() {
1962 if (mEmailTypesMap != null) {
1963 return;
1964 }
1965 mEmailTypesMap = new HashMap<String, Integer>();
1967 mEmailTypesMap.put("home", Email.TYPE_HOME);
1968 mEmailTypesMap.put("mobile", Email.TYPE_MOBILE);
1969 mEmailTypesMap.put("work", Email.TYPE_WORK);
1970 }
1972 private int getWebsiteType(String webisteType) {
1973 initWebsiteTypesMap();
1974 Integer type = mWebsiteTypesMap.get(webisteType.toLowerCase());
1975 return (type != null ? Integer.valueOf(type) : Website.TYPE_CUSTOM);
1976 }
1978 private void initWebsiteTypesMap() {
1979 if (mWebsiteTypesMap != null) {
1980 return;
1981 }
1982 mWebsiteTypesMap = new HashMap<String, Integer>();
1984 mWebsiteTypesMap.put("homepage", Website.TYPE_HOMEPAGE);
1985 mWebsiteTypesMap.put("blog", Website.TYPE_BLOG);
1986 mWebsiteTypesMap.put("profile", Website.TYPE_PROFILE);
1987 mWebsiteTypesMap.put("home", Website.TYPE_HOME);
1988 mWebsiteTypesMap.put("work", Website.TYPE_WORK);
1989 mWebsiteTypesMap.put("ftp", Website.TYPE_FTP);
1990 }
1992 private int getImType(String imType) {
1993 initImTypesMap();
1994 Integer type = mImTypesMap.get(imType.toLowerCase());
1995 return (type != null ? Integer.valueOf(type) : Im.TYPE_CUSTOM);
1996 }
1998 private void initImTypesMap() {
1999 if (mImTypesMap != null) {
2000 return;
2001 }
2002 mImTypesMap = new HashMap<String, Integer>();
2004 mImTypesMap.put("home", Im.TYPE_HOME);
2005 mImTypesMap.put("work", Im.TYPE_WORK);
2006 }
2008 private String[] getAllColumns() {
2009 return new String[] {Entity.DATA_ID, Data.MIMETYPE, Data.IS_SUPER_PRIMARY,
2010 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4,
2011 Data.DATA5, Data.DATA6, Data.DATA7, Data.DATA8,
2012 Data.DATA9, Data.DATA10, Data.DATA11, Data.DATA12,
2013 Data.DATA13, Data.DATA14, Data.DATA15};
2014 }
2015 }