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.runtime.extension.api.contacts;
7 import android.content.ContentResolver;
8 import android.database.Cursor;
9 import android.provider.ContactsContract.Data;
10 import android.provider.ContactsContract.CommonDataKinds.Email;
11 import android.provider.ContactsContract.CommonDataKinds.Event;
12 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
13 import android.provider.ContactsContract.CommonDataKinds.Im;
14 import android.provider.ContactsContract.CommonDataKinds.Nickname;
15 import android.provider.ContactsContract.CommonDataKinds.Note;
16 import android.provider.ContactsContract.CommonDataKinds.Organization;
17 import android.provider.ContactsContract.CommonDataKinds.Phone;
18 import android.provider.ContactsContract.CommonDataKinds.Photo;
19 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
20 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
21 import android.provider.ContactsContract.CommonDataKinds.Website;
22 import android.util.Log;
23 import android.util.Pair;
25 import java.util.ArrayList;
26 import java.util.HashSet;
27 import java.util.LinkedHashMap;
28 import java.util.List;
32 import org.json.JSONArray;
33 import org.json.JSONException;
34 import org.json.JSONObject;
37 * This class searches contacts by given options.
39 public class ContactFinder {
40 private ContactUtils mUtils;
41 private static final String TAG = "ContactFinder";
43 public ContactFinder(ContentResolver resolver) {
44 mUtils = new ContactUtils(resolver);
47 public static class FindOption {
49 public String[] mWhereArgs;
50 public String mSortOrder;
52 public FindOption(String where, String[] whereArgs, String sortOrder) {
54 mWhereArgs = whereArgs;
55 mSortOrder = sortOrder;
59 private class ContactData {
60 public String lastUpdated;
61 public JSONObject oName;
62 public JSONArray aEmails;
63 public JSONArray aPhotos;
64 public JSONArray aUrls;
65 public JSONArray aCategories;
66 public JSONArray aAddresses;
67 public JSONArray aNumbers;
68 public JSONArray aOrganizations;
69 public JSONArray aJobTitles;
70 public String birthday;
71 public JSONArray aNotes;
72 public JSONArray aImpp;
73 public String anniversary;
75 public JSONObject ensurePut(long id) {
76 JSONObject o = new JSONObject();
79 } catch (JSONException e) {
80 Log.e(TAG, "ensurePut - Failed to build json data: " + e.toString());
82 ensurePut(o, "name", oName);
83 ensurePut(o, "lastUpdated", lastUpdated);
84 ensurePut(o, "emails", aEmails);
85 ensurePut(o, "photos", aPhotos);
86 ensurePut(o, "urls", aUrls);
87 ensurePut(o, "categories", aCategories);
88 ensurePut(o, "addresses", aAddresses);
89 ensurePut(o, "phoneNumbers", aNumbers);
90 ensurePut(o, "organizations", aOrganizations);
91 ensurePut(o, "jobTitles", aJobTitles);
92 ensurePut(o, "birthday", birthday);
93 ensurePut(o, "notes", aNotes);
94 ensurePut(o, "impp", aImpp);
95 ensurePut(o, "anniversary", anniversary);
96 ensurePut(o, "gender", gender);
99 private <T> void ensurePut(JSONObject o, String jsonName, T t) {
101 if (t != null) o.put(jsonName, t);
102 } catch (JSONException e) {
103 Log.e(TAG, "ensurePut - Failed to add json data: " + e.toString());
108 private JSONObject addString(JSONObject o, Cursor c, String jsonName, String dataName) {
110 final String value = c.getString(c.getColumnIndex(dataName));
111 if (o == null) o = new JSONObject();
112 if (value != null) o.put(jsonName, value);
113 } catch (JSONException e) {
114 Log.e(TAG, "addString - Failed to build json data: " + e.toString());
119 private JSONArray addString(JSONArray array, Cursor c, String dataName) {
120 if (array == null) array = new JSONArray();
121 final String value = c.getString(c.getColumnIndex(dataName));
122 if (value != null) array.put(value);
126 private JSONObject addArrayTop(JSONObject o, Cursor c,
127 String jsonName, String dataName, Map<String, Integer> typeValuesMap) {
128 final String nameString = ContactUtils.getKeyFromValue(
129 typeValuesMap, Integer.valueOf(c.getString(c.getColumnIndex(dataName))));
130 return ensureAddArrayTop(o, c, jsonName, nameString);
133 private JSONObject addArrayTop(JSONObject o, Cursor c,
134 String jsonName, String dataName) {
135 final String nameString = c.getString(c.getColumnIndex(dataName));
136 return ensureAddArrayTop(o, c, jsonName, nameString);
139 private JSONObject ensureAddArrayTop(JSONObject o, Cursor c,
140 String jsonName, String nameString) {
142 if (o == null) o = new JSONObject();
143 if (nameString != null) {
144 JSONArray nameArray = new JSONArray();
145 nameArray.put(nameString);
146 o.put(jsonName, nameArray);
148 } catch (JSONException e) {
149 Log.e(TAG, "ensureAddArrayTop - Failed to build json data: " + e.toString());
154 private JSONArray addTypeArray(JSONArray array, Cursor c, String data,
155 Map<String, String> typeMap, Map<String, Integer> typeValuesMap) {
157 if (array == null) array = new JSONArray();
158 final String primary = typeMap.get("isSuperPrimary");
159 final String preferred =
160 (c.getString(c.getColumnIndex(primary)).equals("1")) ? "true" : "false";
161 JSONObject o = new JSONObject();
162 o.put("preferred", preferred);
163 addArrayTop(o, c, "types", typeMap.get("type"), typeValuesMap);
164 String value = c.getString(c.getColumnIndex(data));
165 if (c.getString(c.getColumnIndex(Data.MIMETYPE)).equals(Im.CONTENT_ITEM_TYPE)) {
166 int protocol = c.getInt(c.getColumnIndex(Im.PROTOCOL));
167 String prefix = ContactUtils.getKeyFromValue(ContactConstants.imProtocolMap, protocol);
168 value = prefix + ':' + value;
170 o.put("value", value);
172 } catch (JSONException e) {
173 Log.e(TAG, "addTypeArray - Failed to build json data: " + e.toString());
178 private Set<String> getContactIds(FindOption findOption) {
179 Set<String> ids = null;
182 c = mUtils.mResolver.query(Data.CONTENT_URI, null, findOption.mWhere,
183 findOption.mWhereArgs, findOption.mSortOrder);
184 ids = new HashSet<String>();
185 while (c.moveToNext()) {
186 ids.add(String.valueOf(c.getLong(c.getColumnIndex(Data.CONTACT_ID))));
189 } catch (SecurityException e) {
190 Log.e(TAG, "getContactIds: " + e.toString());
193 if (c != null) c.close();
198 * Generate a string to be used as sort order by SQL clause
199 * @param sortBy e.g. "givenNames", "phoneNumbers"
200 * @param sortOrder e.g. "descending"
201 * @return Data name with SQL clause: "StructuredName.GIVEN_NAME DESC, Phone.NUMBER DESC"
203 private String getSortOrder(List<String> sortBy, String sortOrder) {
204 if (sortOrder == null) return null;
207 if (sortOrder.equals("ascending")) {
209 } else if (sortOrder.equals("descending")) {
214 for (String s : sortBy) {
215 Pair<String, String> fields = ContactConstants.contactDataMap.get(s);
216 if (fields == null) continue;
217 order += fields.first + suffix + ",";
219 return (order != "") ? order.substring(0, order.length()-1) : null;
222 //TODO(hdq): Currently this function doesn't support multi-column sorting.
223 private JSONArray getContacts(
224 Set<String> contactIds, String sortOrder, String sortByMimeType, Long resultsLimit) {
225 // Get all records of given contactIds.
226 // For example, sort by ascending:
227 // -----------------------------
229 // -----------------------------
230 // 60 +34600000000 phone_v2
231 // 59 +34698765432 phone_v2
232 // 59 David1 Smith name
233 // 60 David2 Smith name
235 String[] whereArgs = null;
236 if (contactIds.size() != 0) {
237 where = Data.CONTACT_ID + " in (" + ContactUtils.makeQuestionMarkList(contactIds) + ")";
238 whereArgs = contactIds.toArray(new String[contactIds.size()]);
241 Map<Long, ContactData> dataMap = null;
243 c = mUtils.mResolver.query(Data.CONTENT_URI, null, where, whereArgs, sortOrder);
244 dataMap = new LinkedHashMap<Long, ContactData>();
246 // Read contact IDs to build the array by sortOrder.
247 if (sortOrder != null) {
248 while (c.moveToNext()) {
249 // We should only check for mimetype of sorting field.
250 // As e.g. above, sort by phone number: [60, 59], by name: [59, 60]
251 String mime = c.getString(c.getColumnIndex(Data.MIMETYPE));
252 if (!mime.equals(sortByMimeType)) continue;
253 long id = c.getLong(c.getColumnIndex(Data.CONTACT_ID));
254 if (!dataMap.containsKey(id)) dataMap.put(id, new ContactData());
259 // Read details of each contacts
260 while (c.moveToNext()) {
261 long id = c.getLong(c.getColumnIndex(Data.CONTACT_ID));
262 if (!dataMap.containsKey(id)) dataMap.put(id, new ContactData());
263 ContactData d = dataMap.get(id);
264 String mime = c.getString(c.getColumnIndex(Data.MIMETYPE));
265 if (mime.equals(ContactConstants.CUSTOM_MIMETYPE_LASTUPDATED)) {
266 d.lastUpdated = c.getString(c.getColumnIndex(Data.DATA1));
267 } else if (mime.equals(StructuredName.CONTENT_ITEM_TYPE)) {
268 d.oName = addString(d.oName, c, "displayName", StructuredName.DISPLAY_NAME);
269 d.oName = addArrayTop(d.oName, c, "honorificPrefixes", StructuredName.PREFIX);
270 d.oName = addArrayTop(d.oName, c, "givenNames", StructuredName.GIVEN_NAME);
271 d.oName = addArrayTop(d.oName, c, "additionalNames", StructuredName.MIDDLE_NAME);
272 d.oName = addArrayTop(d.oName, c, "familyNames", StructuredName.FAMILY_NAME);
273 d.oName = addArrayTop(d.oName, c, "honorificSuffixes", StructuredName.SUFFIX);
274 } else if (mime.equals(Nickname.CONTENT_ITEM_TYPE)) {
275 d.oName = addArrayTop(d.oName, c, "nicknames", Nickname.NAME);
276 } else if (mime.equals(Email.CONTENT_ITEM_TYPE)) {
277 d.aEmails = addTypeArray(d.aEmails, c, Email.DATA,
278 ContactConstants.emailTypeMap,
279 ContactConstants.emailTypeValuesMap);
280 } else if (mime.equals(Photo.CONTENT_ITEM_TYPE)) {
281 d.aPhotos = addString(d.aPhotos, c, Photo.PHOTO);
282 } else if (mime.equals(Website.CONTENT_ITEM_TYPE)) {
283 d.aUrls = addTypeArray(d.aUrls, c, Website.DATA,
284 ContactConstants.websiteTypeMap,
285 ContactConstants.websiteTypeValuesMap);
286 } else if (mime.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
287 String title = mUtils.getGroupTitle(
288 c.getString(c.getColumnIndex(GroupMembership.GROUP_ROW_ID)));
290 if (d.aCategories == null) d.aCategories = new JSONArray();
291 d.aCategories.put(title);
293 } else if (mime.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
294 d.aAddresses = addTypeArray(d.aAddresses, c, StructuredPostal.DATA,
295 ContactConstants.addressTypeMap,
296 ContactConstants.addressTypeValuesMap);
297 } else if (mime.equals(Phone.CONTENT_ITEM_TYPE)) {
298 d.aNumbers = addTypeArray(d.aNumbers, c, Phone.DATA,
299 ContactConstants.phoneTypeMap,
300 ContactConstants.phoneTypeValuesMap);
301 } else if (mime.equals(Organization.CONTENT_ITEM_TYPE)) {
302 d.aOrganizations = addString(d.aOrganizations, c, Organization.COMPANY);
303 } else if (mime.equals(Organization.CONTENT_ITEM_TYPE)) {
304 d.aJobTitles = addString(d.aJobTitles, c, Organization.TITLE);
305 } else if (mime.equals(Event.CONTENT_ITEM_TYPE)) {
306 int type = Integer.valueOf(c.getString(c.getColumnIndex(Event.TYPE)));
307 if (type == Event.TYPE_BIRTHDAY) {
308 d.birthday = c.getString(c.getColumnIndex(Event.START_DATE));
309 } else if (type == Event.TYPE_ANNIVERSARY) {
310 d.anniversary = c.getString(c.getColumnIndex(Event.START_DATE));
312 } else if (mime.equals(Note.CONTENT_ITEM_TYPE)) {
313 d.aNotes = addString(d.aNotes, c, Note.NOTE);
314 } else if (mime.equals(Im.CONTENT_ITEM_TYPE)) {
315 d.aImpp = addTypeArray(d.aImpp, c, Im.DATA, ContactConstants.imTypeMap,
316 ContactConstants.imTypeValuesMap);
317 } else if (mime.equals(ContactConstants.CUSTOM_MIMETYPE_GENDER)) {
318 d.gender = c.getString(c.getColumnIndex(Data.DATA1));
321 } catch (Exception e) {
322 if (e instanceof NumberFormatException || e instanceof SecurityException) {
323 Log.e(TAG, "getContacts: " + e.toString());
324 return new JSONArray();
326 throw new RuntimeException(e);
329 if (c != null) c.close();
333 JSONArray returnArray = new JSONArray();
334 for (Map.Entry<Long, ContactData> entry : dataMap.entrySet()) {
335 if (resultsLimit != null && ++i > resultsLimit) break;
336 JSONObject o = entry.getValue().ensurePut(entry.getKey());
342 private FindOption createFindIDOption(String findString) {
343 ContactJson findJson = new ContactJson(findString);
344 String value = (findString != null) ? findJson.getString("value") : null;
345 if (value == null || value.equals("") || findString.equals("")) {
346 return new FindOption(null, null, null);
348 List<String> args = new ArrayList<String>();
349 List<String> fields = findJson.getStringArray("fields");
350 String operator = findJson.getString("operator");
351 if (operator == null) {
352 return new FindOption(null, null, null);
353 } else if (operator.equals("is")) {
355 } else if (operator.equals("contains")) {
357 value = "%" + value + "%";
359 Log.e(TAG, "find - Wrong Operator: ["+operator+"], should be 'is' or 'contains'");
363 for (String field : fields) {
364 // E.g. for "givenName" should check column of "givenNames".
365 String column = ContactConstants.findFieldMap.get(field);
366 // Skip invalid fields
367 if (column == null) continue;
368 android.util.Pair<String, String> name = ContactConstants.contactDataMap.get(column);
369 // E.g. first is GIVEN_NAME, second is StructuredName.MIMETYPE
370 where += name.first + operator + " ? AND " + Data.MIMETYPE + " = ? or ";
372 args.add(name.second);
374 if (where == "") return new FindOption(null, null, null);
375 // Remove the "or " which appended in the loop above.
376 where = where.substring(0, where.length()-3);
377 String[] whereArgs = args.toArray(new String[args.size()]);
378 return new FindOption(where, whereArgs, null);
382 public JSONArray find(String findString) {
383 Set<String> ids = getContactIds(createFindIDOption(findString));
384 if (ids == null) return new JSONArray();
385 ContactJson findJson = new ContactJson(findString);
386 List<String> sortBy = findJson.getStringArray("sortBy");
387 String order = getSortOrder(sortBy, findJson.getString("sortOrder"));
388 String orderMimeType = (order == null) ? null :
389 ContactConstants.contactDataMap.get(sortBy.get(0)).second;
390 String resultsLimit = findJson.getString("resultsLimit");
391 Long resultsLimitLong = (resultsLimit == null) ? null : Long.valueOf(resultsLimit);
392 return getContacts(ids, order, orderMimeType, resultsLimitLong);