1 // Copyright (c) 2013 Intel Corporation. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.xwalk.core.extension.api.contacts;
7 import android.content.ContentResolver;
8 import android.database.Cursor;
9 import android.os.Build.VERSION;
10 import android.os.Build.VERSION_CODES;
11 import android.provider.ContactsContract.Data;
12 import android.provider.ContactsContract.CommonDataKinds.Email;
13 import android.provider.ContactsContract.CommonDataKinds.Event;
14 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
15 import android.provider.ContactsContract.CommonDataKinds.Im;
16 import android.provider.ContactsContract.CommonDataKinds.Nickname;
17 import android.provider.ContactsContract.CommonDataKinds.Note;
18 import android.provider.ContactsContract.CommonDataKinds.Organization;
19 import android.provider.ContactsContract.CommonDataKinds.Phone;
20 import android.provider.ContactsContract.CommonDataKinds.Photo;
21 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
22 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
23 import android.provider.ContactsContract.CommonDataKinds.Website;
24 import android.util.Log;
25 import android.util.Pair;
27 import java.util.ArrayList;
28 import java.util.HashSet;
29 import java.util.LinkedHashMap;
30 import java.util.List;
34 import org.json.JSONArray;
35 import org.json.JSONException;
36 import org.json.JSONObject;
39 * This class searches contacts by given options.
41 public class ContactFinder {
42 private ContactUtils mUtils;
43 private static final String TAG = "ContactFinder";
45 public ContactFinder(ContentResolver resolver) {
46 mUtils = new ContactUtils(resolver);
49 public static class FindOption {
51 public String[] mWhereArgs;
52 public String mSortOrder;
54 public FindOption(String where, String[] whereArgs, String sortOrder) {
56 mWhereArgs = whereArgs;
57 mSortOrder = sortOrder;
61 private class ContactData {
62 public String lastUpdated;
63 public JSONObject oName;
64 public JSONArray aEmails;
65 public JSONArray aPhotos;
66 public JSONArray aUrls;
67 public JSONArray aCategories;
68 public JSONArray aAddresses;
69 public JSONArray aNumbers;
70 public JSONArray aOrganizations;
71 public JSONArray aJobTitles;
72 public String birthday;
73 public JSONArray aNotes;
74 public JSONArray aImpp;
75 public String anniversary;
77 public JSONObject ensurePut(long id) {
78 JSONObject o = new JSONObject();
81 } catch (JSONException e) {
82 Log.e(TAG, "ensurePut - Failed to build json data: " + e.toString());
84 ensurePut(o, "name", oName);
85 ensurePut(o, "lastUpdated", lastUpdated);
86 ensurePut(o, "emails", aEmails);
87 ensurePut(o, "photos", aPhotos);
88 ensurePut(o, "urls", aUrls);
89 ensurePut(o, "categories", aCategories);
90 ensurePut(o, "addresses", aAddresses);
91 ensurePut(o, "phoneNumbers", aNumbers);
92 ensurePut(o, "organizations", aOrganizations);
93 ensurePut(o, "jobTitles", aJobTitles);
94 ensurePut(o, "birthday", birthday);
95 ensurePut(o, "notes", aNotes);
96 ensurePut(o, "impp", aImpp);
97 ensurePut(o, "anniversary", anniversary);
98 ensurePut(o, "gender", gender);
101 private <T> void ensurePut(JSONObject o, String jsonName, T t) {
103 if (t != null) o.put(jsonName, t);
104 } catch (JSONException e) {
105 Log.e(TAG, "ensurePut - Failed to add json data: " + e.toString());
110 private JSONObject addString(JSONObject o, Cursor c, String jsonName, String dataName) {
112 final String value = c.getString(c.getColumnIndex(dataName));
113 if (o == null) o = new JSONObject();
114 if (value != null) o.put(jsonName, value);
115 } catch (JSONException e) {
116 Log.e(TAG, "addString - Failed to build json data: " + e.toString());
121 private JSONArray addString(JSONArray array, Cursor c, String dataName) {
122 if (array == null) array = new JSONArray();
123 final String value = c.getString(c.getColumnIndex(dataName));
124 if (value != null) array.put(value);
128 private JSONObject addArrayTop(JSONObject o, Cursor c,
129 String jsonName, String dataName, Map<String, Integer> typeValuesMap) {
130 final String nameString = ContactUtils.getKeyFromValue(
131 typeValuesMap, Integer.valueOf(c.getString(c.getColumnIndex(dataName))));
132 return ensureAddArrayTop(o, c, jsonName, nameString);
135 private JSONObject addArrayTop(JSONObject o, Cursor c,
136 String jsonName, String dataName) {
137 final String nameString = c.getString(c.getColumnIndex(dataName));
138 return ensureAddArrayTop(o, c, jsonName, nameString);
141 private JSONObject ensureAddArrayTop(JSONObject o, Cursor c,
142 String jsonName, String nameString) {
144 if (o == null) o = new JSONObject();
145 if (nameString != null) {
146 JSONArray nameArray = new JSONArray();
147 nameArray.put(nameString);
148 o.put(jsonName, nameArray);
150 } catch (JSONException e) {
151 Log.e(TAG, "ensureAddArrayTop - Failed to build json data: " + e.toString());
156 private JSONArray addTypeArray(JSONArray array, Cursor c, String data,
157 Map<String, String> typeMap, Map<String, Integer> typeValuesMap) {
159 if (array == null) array = new JSONArray();
160 final String primary = typeMap.get("isSuperPrimary");
161 final String preferred =
162 (c.getString(c.getColumnIndex(primary)).equals("1")) ? "true" : "false";
163 JSONObject o = new JSONObject();
164 o.put("preferred", preferred);
165 addArrayTop(o, c, "types", typeMap.get("type"), typeValuesMap);
166 String value = c.getString(c.getColumnIndex(data));
167 if (c.getString(c.getColumnIndex(Data.MIMETYPE)).equals(Im.CONTENT_ITEM_TYPE)) {
168 int protocol = c.getInt(c.getColumnIndex(Im.PROTOCOL));
169 String prefix = ContactUtils.getKeyFromValue(ContactConstants.imProtocolMap, protocol);
170 value = prefix + ':' + value;
172 o.put("value", value);
174 } catch (JSONException e) {
175 Log.e(TAG, "addTypeArray - Failed to build json data: " + e.toString());
180 private Set<String> getContactIds(FindOption findOption) {
181 Set<String> ids = null;
184 c = mUtils.mResolver.query(Data.CONTENT_URI, null, findOption.mWhere,
185 findOption.mWhereArgs, findOption.mSortOrder);
186 ids = new HashSet<String>();
187 while (c.moveToNext()) {
188 ids.add(String.valueOf(c.getLong(c.getColumnIndex(Data.CONTACT_ID))));
191 } catch (SecurityException e) {
192 Log.e(TAG, "getContactIds: " + e.toString());
195 if (c != null) c.close();
200 * Generate a string to be used as sort order by SQL clause
201 * @param sortBy e.g. "givenNames", "phoneNumbers"
202 * @param sortOrder e.g. "descending"
203 * @return Data name with SQL clause: "StructuredName.GIVEN_NAME DESC, Phone.NUMBER DESC"
205 private String getSortOrder(List<String> sortBy, String sortOrder) {
206 if (sortOrder == null) return null;
209 if (sortOrder.equals("ascending")) {
211 } else if (sortOrder.equals("descending")) {
216 for (String s : sortBy) {
217 Pair<String, String> fields = ContactConstants.contactDataMap.get(s);
218 if (fields == null) continue;
219 order += fields.first + suffix + ",";
221 return (order != "") ? order.substring(0, order.length()-1) : null;
224 //TODO(hdq): Currently this function doesn't support multi-column sorting.
225 private JSONArray getContacts(
226 Set<String> contactIds, String sortOrder, String sortByMimeType, Long resultsLimit) {
227 // Get all records of given contactIds.
228 // For example, sort by ascending:
229 // -----------------------------
231 // -----------------------------
232 // 60 +34600000000 phone_v2
233 // 59 +34698765432 phone_v2
234 // 59 David1 Smith name
235 // 60 David2 Smith name
237 String[] whereArgs = null;
238 if (contactIds.size() != 0) {
239 where = Data.CONTACT_ID + " in (" + ContactUtils.makeQuestionMarkList(contactIds) + ")";
240 whereArgs = contactIds.toArray(new String[contactIds.size()]);
243 Map<Long, ContactData> dataMap = null;
245 c = mUtils.mResolver.query(Data.CONTENT_URI, null, where, whereArgs, sortOrder);
246 dataMap = new LinkedHashMap<Long, ContactData>();
248 // Read contact IDs to build the array by sortOrder.
249 if (sortOrder != null) {
250 while (c.moveToNext()) {
251 // We should only check for mimetype of sorting field.
252 // As e.g. above, sort by phone number: [60, 59], by name: [59, 60]
253 String mime = c.getString(c.getColumnIndex(Data.MIMETYPE));
254 if (!mime.equals(sortByMimeType)) continue;
255 long id = c.getLong(c.getColumnIndex(Data.CONTACT_ID));
256 if (!dataMap.containsKey(id)) dataMap.put(id, new ContactData());
261 // Read details of each contacts
262 while (c.moveToNext()) {
263 long id = c.getLong(c.getColumnIndex(Data.CONTACT_ID));
264 if (!dataMap.containsKey(id)) dataMap.put(id, new ContactData());
265 ContactData d = dataMap.get(id);
266 if (d.lastUpdated == null && VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
267 d.lastUpdated = mUtils.getLastUpdated(id);
269 String mime = c.getString(c.getColumnIndex(Data.MIMETYPE));
270 if (mime.equals(StructuredName.CONTENT_ITEM_TYPE)) {
271 d.oName = addString(d.oName, c, "displayName", StructuredName.DISPLAY_NAME);
272 d.oName = addArrayTop(d.oName, c, "honorificPrefixes", StructuredName.PREFIX);
273 d.oName = addArrayTop(d.oName, c, "givenNames", StructuredName.GIVEN_NAME);
274 d.oName = addArrayTop(d.oName, c, "additionalNames", StructuredName.MIDDLE_NAME);
275 d.oName = addArrayTop(d.oName, c, "familyNames", StructuredName.FAMILY_NAME);
276 d.oName = addArrayTop(d.oName, c, "honorificSuffixes", StructuredName.SUFFIX);
277 } else if (mime.equals(Nickname.CONTENT_ITEM_TYPE)) {
278 d.oName = addArrayTop(d.oName, c, "nicknames", Nickname.NAME);
279 } else if (mime.equals(Email.CONTENT_ITEM_TYPE)) {
280 d.aEmails = addTypeArray(d.aEmails, c, Email.DATA,
281 ContactConstants.emailTypeMap,
282 ContactConstants.emailTypeValuesMap);
283 } else if (mime.equals(Photo.CONTENT_ITEM_TYPE)) {
284 d.aPhotos = addString(d.aPhotos, c, Photo.PHOTO);
285 } else if (mime.equals(Website.CONTENT_ITEM_TYPE)) {
286 d.aUrls = addTypeArray(d.aUrls, c, Website.DATA,
287 ContactConstants.websiteTypeMap,
288 ContactConstants.websiteTypeValuesMap);
289 } else if (mime.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
290 String title = mUtils.getGroupTitle(
291 c.getString(c.getColumnIndex(GroupMembership.GROUP_ROW_ID)));
293 if (d.aCategories == null) d.aCategories = new JSONArray();
294 d.aCategories.put(title);
296 } else if (mime.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
297 d.aAddresses = addTypeArray(d.aAddresses, c, StructuredPostal.DATA,
298 ContactConstants.addressTypeMap,
299 ContactConstants.addressTypeValuesMap);
300 } else if (mime.equals(Phone.CONTENT_ITEM_TYPE)) {
301 d.aNumbers = addTypeArray(d.aNumbers, c, Phone.DATA,
302 ContactConstants.phoneTypeMap,
303 ContactConstants.phoneTypeValuesMap);
304 } else if (mime.equals(Organization.CONTENT_ITEM_TYPE)) {
305 d.aOrganizations = addString(d.aOrganizations, c, Organization.COMPANY);
306 } else if (mime.equals(Organization.CONTENT_ITEM_TYPE)) {
307 d.aJobTitles = addString(d.aJobTitles, c, Organization.TITLE);
308 } else if (mime.equals(Event.CONTENT_ITEM_TYPE)) {
309 int type = Integer.valueOf(c.getString(c.getColumnIndex(Event.TYPE)));
310 if (type == Event.TYPE_BIRTHDAY) {
311 d.birthday = c.getString(c.getColumnIndex(Event.START_DATE));
312 } else if (type == Event.TYPE_ANNIVERSARY) {
313 d.anniversary = c.getString(c.getColumnIndex(Event.START_DATE));
315 } else if (mime.equals(Note.CONTENT_ITEM_TYPE)) {
316 d.aNotes = addString(d.aNotes, c, Note.NOTE);
317 } else if (mime.equals(Im.CONTENT_ITEM_TYPE)) {
318 d.aImpp = addTypeArray(d.aImpp, c, Im.DATA, ContactConstants.imTypeMap,
319 ContactConstants.imTypeValuesMap);
320 } else if (mime.equals(ContactConstants.CUSTOM_MIMETYPE_GENDER)) {
321 d.gender = c.getString(c.getColumnIndex(Data.DATA1));
324 } catch (Exception e) {
325 if (e instanceof NumberFormatException || e instanceof SecurityException) {
326 Log.e(TAG, "getContacts: " + e.toString());
327 return new JSONArray();
329 throw new RuntimeException(e);
332 if (c != null) c.close();
336 JSONArray returnArray = new JSONArray();
337 for (Map.Entry<Long, ContactData> entry : dataMap.entrySet()) {
338 if (resultsLimit != null && ++i > resultsLimit) break;
339 JSONObject o = entry.getValue().ensurePut(entry.getKey());
345 private FindOption createFindIDOption(String findString) {
346 ContactJson findJson = new ContactJson(findString);
347 String value = (findString != null) ? findJson.getString("value") : null;
348 if (value == null || value.equals("") || findString.equals("")) {
349 return new FindOption(null, null, null);
351 List<String> args = new ArrayList<String>();
352 List<String> fields = findJson.getStringArray("fields");
353 String operator = findJson.getString("operator");
354 if (operator == null) {
355 return new FindOption(null, null, null);
356 } else if (operator.equals("is")) {
358 } else if (operator.equals("contains")) {
360 value = "%" + value + "%";
362 Log.e(TAG, "find - Wrong Operator: ["+operator+"], should be 'is' or 'contains'");
366 for (String field : fields) {
367 // E.g. for "givenName" should check column of "givenNames".
368 String column = ContactConstants.findFieldMap.get(field);
369 // Skip invalid fields
370 if (column == null) continue;
371 android.util.Pair<String, String> name = ContactConstants.contactDataMap.get(column);
372 // E.g. first is GIVEN_NAME, second is StructuredName.MIMETYPE
373 where += name.first + operator + " ? AND " + Data.MIMETYPE + " = ? or ";
375 args.add(name.second);
377 if (where == "") return new FindOption(null, null, null);
378 // Remove the "or " which appended in the loop above.
379 where = where.substring(0, where.length()-3);
380 String[] whereArgs = args.toArray(new String[args.size()]);
381 return new FindOption(where, whereArgs, null);
385 public JSONArray find(String findString) {
386 Set<String> ids = getContactIds(createFindIDOption(findString));
387 if (ids == null) return new JSONArray();
388 ContactJson findJson = new ContactJson(findString);
389 List<String> sortBy = findJson.getStringArray("sortBy");
390 String order = getSortOrder(sortBy, findJson.getString("sortOrder"));
391 String orderMimeType = (order == null) ? null :
392 ContactConstants.contactDataMap.get(sortBy.get(0)).second;
393 String resultsLimit = findJson.getString("resultsLimit");
394 Long resultsLimitLong = (resultsLimit == null) ? null : Long.valueOf(resultsLimit);
395 return getContacts(ids, order, orderMimeType, resultsLimitLong);