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.ContentProviderOperation;
8 import android.content.ContentProviderOperation.Builder;
9 import android.content.ContentResolver;
10 import android.content.OperationApplicationException;
11 import android.os.RemoteException;
12 import android.provider.ContactsContract;
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.StructuredName;
18 import android.provider.ContactsContract.Data;
19 import android.util.Log;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
27 import org.json.JSONArray;
28 import org.json.JSONException;
29 import org.json.JSONObject;
31 import org.xwalk.core.extension.api.contacts.ContactConstants.ContactMap;
34 * This class saves contacts according to a given JSONString.
36 public class ContactSaver {
37 private ContactUtils mUtils;
38 private static final String TAG = "ContactSaver";
40 private JSONObject mContact;
41 private ContactJson mJson;
43 private boolean mIsUpdate;
44 private ArrayList<ContentProviderOperation> mOps;
46 public ContactSaver(ContentResolver resolver) {
47 mUtils = new ContactUtils(resolver);
51 private Builder newUpdateBuilder(String mimeType) {
52 Builder builder = ContentProviderOperation.newUpdate(Data.CONTENT_URI);
53 builder.withSelection(
54 Data.CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
55 new String[]{mId, mimeType});
60 private Builder newInsertBuilder(String mimeType) {
61 Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
62 builder.withValueBackReference(Data.RAW_CONTACT_ID, 0);
63 builder.withValue(Data.MIMETYPE, mimeType);
67 // Add a new field to an existing contact
68 private Builder newInsertFieldBuilder(String mimeType) {
69 String rawId = mUtils.getRawId(mId);
71 Log.e(TAG, "Failed to create builder to insert field of " + mId);
74 Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
75 builder.withValue(Data.RAW_CONTACT_ID, mUtils.getRawId(mId));
76 builder.withValue(Data.MIMETYPE, mimeType);
80 // Add a new contact or add a new field to an existing contact
81 private Builder newInsertContactOrFieldBuilder(String mimeType) {
82 return mIsUpdate ? newInsertFieldBuilder(mimeType)
83 : newInsertBuilder(mimeType);
86 // Add a new contact or update a contact
87 private Builder newBuilder(String mimeType) {
88 return mIsUpdate ? newUpdateBuilder(mimeType)
89 : newInsertBuilder(mimeType);
92 // Build by a data array with types
93 private void buildByArray(ContactMap contactMap) {
94 if (!mContact.has(contactMap.mName)) {
98 // When updating multiple records of one MIMEType,
99 // we need to flush the old records and then insert new ones later.
101 // For example, it is possible that a contact has several phone numbers,
102 // in data table it will be like this:
103 // CONTACT_ID MIMETYPE TYPE DATA1
104 // ------------------------------------------
105 // 374 Phone_v2 Work +4412345678
106 // 374 Phone_v2 Work +4402778877
107 // In this case if we update by SQL selection clause directly,
108 // will get two same records of last update value.
111 mUtils.cleanByMimeType(mId, contactMap.mMimeType);
114 final JSONArray fields = mContact.getJSONArray(contactMap.mName);
115 for (int i = 0; i < fields.length(); ++i) {
116 ContactJson json = new ContactJson(fields.getJSONObject(i));
117 List<String> typeList = json.getStringArray("types");
118 if (typeList != null && !typeList.isEmpty()) {
119 // Currently we can't store multiple types in Android
120 final String type = typeList.get(0);
121 final Integer iType = contactMap.mTypeValueMap.get(type);
123 Builder builder = newInsertContactOrFieldBuilder(contactMap.mMimeType);
124 if (builder == null) return;
126 if (json.getBoolean("preferred")) {
127 builder.withValue(contactMap.mTypeMap.get("isPrimary"), 1);
128 builder.withValue(contactMap.mTypeMap.get("isSuperPrimary"), 1);
131 builder.withValue(contactMap.mTypeMap.get("type"), iType);
133 for (Map.Entry<String, String> entry : contactMap.mDataMap.entrySet()) {
134 String value = json.getString(entry.getValue());
135 if (contactMap.mName.equals("impp")) {
136 int colonIdx = value.indexOf(':');
137 // An impp must indicate its protocol type by ':'
138 if (-1 == colonIdx) continue;
139 String protocol = value.substring(0, colonIdx);
140 builder.withValue(Im.PROTOCOL,
141 ContactConstants.imProtocolMap.get(protocol));
142 value = value.substring(colonIdx+1);
144 builder.withValue(entry.getKey(), value);
146 mOps.add(builder.build());
149 } catch (JSONException e) {
150 Log.e(TAG, "Failed to parse json data of " + contactMap.mName + ": " + e.toString());
154 // Build by a data array without types
155 private void buildByArray(String mimeType, String data, List<String> dataEntries) {
157 mUtils.cleanByMimeType(mId, mimeType);
159 for (String entry : dataEntries) {
160 Builder builder = newInsertContactOrFieldBuilder(mimeType);
161 if (builder == null) return;
162 builder.withValue(data, entry);
163 mOps.add(builder.build());
167 private void buildByArray(ContactMap contactMap, String data, List<String> dataEntries) {
168 if (mContact.has(contactMap.mName)) {
169 buildByArray(contactMap.mMimeType, data, dataEntries);
173 private void buildByDate(String name, String mimeType, String data) {
174 buildByDate(name, mimeType, data, null, 0);
177 private void buildByDate(String name, String mimeType, String data, String type, int dateType) {
178 if (!mContact.has(name)) return;
180 final String fullDateString = mJson.getString(name);
181 final String dateString = mUtils.dateTrim(fullDateString);
182 Builder builder = newBuilder(mimeType);
183 builder.withValue(data, dateString);
184 if (type != null) builder.withValue(type, dateType);
185 mOps.add(builder.build());
188 private void buildByEvent(String eventName, int eventType) {
189 buildByDate(eventName, Event.CONTENT_ITEM_TYPE, Event.START_DATE, Event.TYPE, eventType);
192 private void buildByContactMapList() {
193 for (ContactMap contactMap : ContactConstants.contactMapList) {
194 if (contactMap.mTypeMap != null) { // Field that has type.
195 buildByArray(contactMap);
196 } else { // Field that contains no type.
197 buildByArray(contactMap, contactMap.mDataMap.get("data"),
198 mJson.getStringArray(contactMap.mName));
203 private void PutToContact(String id) {
204 if (id == null) return;
206 mContact.put("id", id);
207 } catch (JSONException e) {
208 Log.e(TAG, "Failed to put id " + id + " into contact" + e.toString());
212 public JSONObject save(String saveString) {
213 mOps = new ArrayList<ContentProviderOperation>();
215 mContact = new JSONObject(saveString);
216 } catch (JSONException e) {
217 Log.e(TAG, "Failed to parse json data: " + e.toString());
218 return new JSONObject();
221 mJson = new ContactJson(mContact);
223 Builder builder = null;
224 mId = mJson.getString("id");
225 mIsUpdate = mUtils.hasID(mId);
227 Set<String> oldRawIds = null;
228 if (!mIsUpdate) { // Create a null record for inserting later
229 oldRawIds = mUtils.getCurrentRawIds();
231 builder = ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI);
232 builder.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null);
233 builder.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
234 mOps.add(builder.build());
238 //-------------------------------------------------
239 // displayName StructuredName.display_name
240 // honorificPrefixes StructuredName.prefix
241 // givenNames StructuredName.given_name
242 // additionalNames StructuredName.middle_name
243 // familyNames StructuredName.family_name
244 // honorificSuffixes StructuredName.suffix
245 // nicknames Nickname.name
246 if (mContact.has("name")) {
247 final JSONObject name = mJson.getObject("name");
248 final ContactJson nameJson = new ContactJson(name);
249 builder = newBuilder(StructuredName.CONTENT_ITEM_TYPE);
250 builder.withValue(StructuredName.DISPLAY_NAME, nameJson.getString("displayName"));
251 //FIXME(hdq): should read all names
252 builder.withValue(StructuredName.FAMILY_NAME, nameJson.getFirstValue("familyNames"));
253 builder.withValue(StructuredName.GIVEN_NAME, nameJson.getFirstValue("givenNames"));
254 builder.withValue(StructuredName.MIDDLE_NAME, nameJson.getFirstValue("additionalNames"));
255 builder.withValue(StructuredName.PREFIX, nameJson.getFirstValue("honorificPrefixes"));
256 builder.withValue(StructuredName.SUFFIX, nameJson.getFirstValue("honorificSuffixes"));
257 mOps.add(builder.build());
259 // Nickname belongs to another mimetype, so we need another builder for it.
260 if (name.has("nicknames")) {
261 builder = newBuilder(Nickname.CONTENT_ITEM_TYPE);
262 builder.withValue(Nickname.NAME, nameJson.getFirstValue("nicknames"));
263 mOps.add(builder.build());
267 if (mContact.has("categories")) {
268 List<String> groupIds = new ArrayList<String>();
269 for (String groupTitle : mJson.getStringArray("categories")) {
270 groupIds.add(mUtils.getEnsuredGroupId(groupTitle));
272 buildByArray(GroupMembership.CONTENT_ITEM_TYPE, GroupMembership.GROUP_ROW_ID, groupIds);
275 if (mContact.has("gender")) {
276 final String gender = mJson.getString("gender");
277 if (Arrays.asList("male", "female", "other", "none", "unknown").contains(gender)) {
278 builder = newBuilder(ContactConstants.CUSTOM_MIMETYPE_GENDER);
279 builder.withValue(Data.DATA1, gender);
280 mOps.add(builder.build());
284 buildByDate("lastUpdated", ContactConstants.CUSTOM_MIMETYPE_LASTUPDATED, Data.DATA1);
285 buildByEvent("birthday", Event.TYPE_BIRTHDAY);
286 buildByEvent("anniversary", Event.TYPE_ANNIVERSARY);
288 buildByContactMapList();
290 // Perform the operation batch
292 mUtils.mResolver.applyBatch(ContactsContract.AUTHORITY, mOps);
293 } catch (Exception e) {
294 if (e instanceof RemoteException ||
295 e instanceof OperationApplicationException ||
296 e instanceof SecurityException) {
297 Log.e(TAG, "Failed to apply batch: " + e.toString());
298 return new JSONObject();
300 throw new RuntimeException(e);
304 // If it is a new contact, need to get and return its auto-generated id.
306 Set<String> newRawIds = mUtils.getCurrentRawIds();
307 if (newRawIds == null) return new JSONObject();
308 newRawIds.removeAll(oldRawIds);
309 if (newRawIds.size() != 1) {
310 Log.e(TAG, "Something wrong after batch applied, "
311 + "new raw ids are: " + newRawIds.toString());
314 String id = mUtils.getId(newRawIds.iterator().next());