mobile/android/base/ContactService.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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);
  1003             Log.i(LOGTAG, "Newly created contact ID: " + newRawContactId);
  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"});
  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;
  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());
  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"});
  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;
  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;
  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);
  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);
  1159             if (givenName != null) {
  1160                 contentValues.put(StructuredName.GIVEN_NAME, givenName);
  1162             if (familyName != null) {
  1163                 contentValues.put(StructuredName.FAMILY_NAME, familyName);
  1165             if (prefix != null) {
  1166                 contentValues.put(StructuredName.PREFIX, prefix);
  1168             if (suffix != null) {
  1169                 contentValues.put(StructuredName.SUFFIX, suffix);
  1172             newContactValues.add(contentValues);
  1176     private void getNicknamesValues(final JSONArray nicknames, List<ContentValues> newContactValues) throws JSONException {
  1177         if (nicknames == null) {
  1178             return;
  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);
  1190     private void getAddressesValues(final JSONArray addresses, List<ContentValues> newContactValues) throws JSONException {
  1191         if (addresses == null) {
  1192             return;
  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));
  1208             } else {
  1209                 newContactValues.add(createAddressContentValues(address, -1, null));
  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);
  1233         if (address.has("pref")) {
  1234             contentValues.put(Data.IS_SUPER_PRIMARY, address.getBoolean("pref") ? 1 : 0);
  1237         return contentValues;
  1240     private void getPhonesValues(final JSONArray phones, List<ContentValues> newContactValues) throws JSONException {
  1241         if (phones == null) {
  1242             return;
  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"));
  1262                     newContactValues.add(contentValues);
  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"));
  1270                 newContactValues.add(contentValues);
  1275     private void getEmailsValues(final JSONArray emails, List<ContentValues> newContactValues) throws JSONException {
  1276         if (emails == null) {
  1277             return;
  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")));
  1296             } else {
  1297                 newContactValues.add(createContentValues(Email.CONTENT_ITEM_TYPE,
  1298                                                          email.optString("value"),
  1299                                                          -1, null, email.optBoolean("pref")));
  1304     private void getPhotosValues(final JSONArray photos, List<ContentValues> newContactValues) throws JSONException {
  1305         if (photos == null) {
  1306             return;
  1309         // TODO: implement this
  1312     private void getNotesValues(final JSONArray notes, List<ContentValues> newContactValues) throws JSONException {
  1313         if (notes == null) {
  1314             return;
  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);
  1325     private void getWebsitesValues(final JSONArray websites, List<ContentValues> newContactValues) throws JSONException {
  1326         if (websites == null) {
  1327             return;
  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")));
  1346             } else {
  1347                 newContactValues.add(createContentValues(Website.CONTENT_ITEM_TYPE,
  1348                                                          website.optString("value"),
  1349                                                          -1, null, website.optBoolean("pref")));
  1354     private void getImsValues(final JSONArray ims, List<ContentValues> newContactValues) throws JSONException {
  1355         if (ims == null) {
  1356             return;
  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")));
  1375             } else {
  1376                 newContactValues.add(createContentValues(Im.CONTENT_ITEM_TYPE,
  1377                                                          im.optString("value"),
  1378                                                          -1, null, im.optBoolean("pref")));
  1383     private void getCategoriesValues(final JSONArray categories, List<ContentValues> newContactValues) throws JSONException {
  1384         if (categories == null) {
  1385             return;
  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;
  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.");
  1411                     continue;
  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);
  1424     private void getEventValues(final String event, final int type, List<ContentValues> newContactValues) {
  1425         if (event == null || event.length() < 11) {
  1426             return;
  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);
  1436     private void getCustomMimetypeValues(final String value, final String mimeType, List<ContentValues> newContactValues) {
  1437         if (value == null || "null".equals(value)) {
  1438             return;
  1441         ContentValues contentValues = new ContentValues();
  1442         contentValues.put(Data.MIMETYPE, mimeType);
  1443         contentValues.put(CUSTOM_DATA_COLUMN, value);
  1444         newContactValues.add(contentValues);
  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);
  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);
  1473         return contentValues;
  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});
  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);
  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]);
  1504             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(subject, callbackMessage.toString()));
  1505         } catch (JSONException e) {
  1506             throw new IllegalArgumentException(e);
  1510     private void registerEventListener(final String event) {
  1511         mEventDispatcher.registerEventListener(event, this);
  1514     private void unregisterEventListener(final String event) {
  1515         mEventDispatcher.unregisterEventListener(event, this);
  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);
  1526         return null;
  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);
  1545         mGotDeviceAccount = true;
  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;
  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);
  1565             });
  1567         mActivity.runOnUiThread(new Runnable() {
  1568             public void run() {
  1569                 builder.show();
  1571         });
  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.");
  1590                 handleMessage.run();
  1592         };
  1594         ThreadUtils.postToBackgroundThread(runnable);
  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;
  1609             final String groupAccountType = cursor.getString(GROUP_ACCOUNT_TYPE);
  1610             if (!groupAccountType.equals(mAccountType)) {
  1611                 continue;
  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;
  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 + ")");
  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);
  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;
  1660         cursor.close();
  1662         return groupId;
  1665     private String getGroupName(long groupId) {
  1666         Cursor cursor = getGroups(Groups._ID + " = " + groupId);
  1668         if (cursor.getCount() == 0) {
  1669             cursor.close();
  1670             return null;
  1673         cursor.moveToPosition(0);
  1674         String groupName = cursor.getString(cursor.getColumnIndex(Groups.TITLE));
  1675         cursor.close();
  1677         return groupName;
  1680     private Cursor getAllGroups() {
  1681         return getGroups(null);
  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 = "";
  1699         return mContentResolver.query(Groups.CONTENT_URI, columns,
  1700                                       Groups.ACCOUNT_TYPE + " NOT NULL AND " +
  1701                                       Groups.ACCOUNT_NAME + " NOT NULL " + selectArg, null, null);
  1704     private long createGroup(String groupName) {
  1705         if (DEBUG) {
  1706             Log.d(LOGTAG, "Creating group: " + groupName);
  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);
  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++;
  1737         cursor.close();
  1739         return ids;
  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};
  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);
  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;
  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));
  1775         return null;
  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);
  1786             if (count != null && count > 0) {
  1787                 return true;
  1791         return false;
  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);
  1801         return array;
  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;
  1811         return false;
  1814     private static int max(int... values) {
  1815         int max = values[0];
  1816         for (int value : values) {
  1817             if (value > max) {
  1818                 max = value;
  1821         return max;
  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);
  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();
  1838         return null;
  1841     private String getColumnNameConstant(String field) {
  1842         initColumnNameConstantsMap();
  1843         return mColumnNameConstantsMap.get(field.toLowerCase());
  1846     private void initColumnNameConstantsMap() {
  1847         if (mColumnNameConstantsMap != null) {
  1848             return;
  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);
  1873     private String getMimeTypeOfField(String field) {
  1874         initMimeTypeConstantsMap();
  1875         return mMimeTypeConstantsMap.get(field.toLowerCase());
  1878     private void initMimeTypeConstantsMap() {
  1879         if (mMimeTypeConstantsMap != null) {
  1880             return;
  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);
  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);
  1910     private void initAddressTypesMap() {
  1911         if (mAddressTypesMap != null) {
  1912             return;
  1914         mAddressTypesMap = new HashMap<String, Integer>();
  1916         mAddressTypesMap.put("home", StructuredPostal.TYPE_HOME);
  1917         mAddressTypesMap.put("work", StructuredPostal.TYPE_WORK);
  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);
  1926     private void initPhoneTypesMap() {
  1927         if (mPhoneTypesMap != null) {
  1928             return;
  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);
  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);
  1961     private void initEmailTypesMap() {
  1962         if (mEmailTypesMap != null) {
  1963             return;
  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);
  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);
  1978     private void initWebsiteTypesMap() {
  1979         if (mWebsiteTypesMap != null) {
  1980             return;
  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);
  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);
  1998     private void initImTypesMap() {
  1999         if (mImTypesMap != null) {
  2000             return;
  2002         mImTypesMap = new HashMap<String, Integer>();
  2004         mImTypesMap.put("home", Im.TYPE_HOME);
  2005         mImTypesMap.put("work", Im.TYPE_WORK);
  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};

mercurial