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, String type, int dateType) {
174 if (!mContact.has(name)) return;
176 final String fullDateString = mJson.getString(name);
177 final String dateString = mUtils.dateTrim(fullDateString);
178 Builder builder = newBuilder(mimeType);
179 builder.withValue(data, dateString);
180 if (type != null) builder.withValue(type, dateType);
181 mOps.add(builder.build());
184 private void buildByEvent(String eventName, int eventType) {
185 buildByDate(eventName, Event.CONTENT_ITEM_TYPE, Event.START_DATE, Event.TYPE, eventType);
188 private void buildByContactMapList() {
189 for (ContactMap contactMap : ContactConstants.contactMapList) {
190 if (contactMap.mTypeMap != null) { // Field that has type.
191 buildByArray(contactMap);
192 } else { // Field that contains no type.
193 buildByArray(contactMap, contactMap.mDataMap.get("data"),
194 mJson.getStringArray(contactMap.mName));
199 private void PutToContact(String name, String value) {
200 if (name == null) return;
202 mContact.put(name, value);
203 } catch (JSONException e) {
204 Log.e(TAG, "Failed to set " + name + " = " + value + " for contact" + e.toString());
208 public JSONObject save(String saveString) {
209 mOps = new ArrayList<ContentProviderOperation>();
211 mContact = new JSONObject(saveString);
212 } catch (JSONException e) {
213 Log.e(TAG, "Failed to parse json data: " + e.toString());
214 return new JSONObject();
217 mJson = new ContactJson(mContact);
219 Builder builder = null;
220 mId = mJson.getString("id");
221 mIsUpdate = mUtils.hasID(mId);
223 Set<String> oldRawIds = null;
224 if (!mIsUpdate) { // Create a null record for inserting later
225 oldRawIds = mUtils.getCurrentRawIds();
227 builder = ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI);
228 builder.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null);
229 builder.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
230 mOps.add(builder.build());
234 //-------------------------------------------------
235 // displayName StructuredName.display_name
236 // honorificPrefixes StructuredName.prefix
237 // givenNames StructuredName.given_name
238 // additionalNames StructuredName.middle_name
239 // familyNames StructuredName.family_name
240 // honorificSuffixes StructuredName.suffix
241 // nicknames Nickname.name
242 if (mContact.has("name")) {
243 final JSONObject name = mJson.getObject("name");
244 final ContactJson nameJson = new ContactJson(name);
245 builder = newBuilder(StructuredName.CONTENT_ITEM_TYPE);
246 builder.withValue(StructuredName.DISPLAY_NAME, nameJson.getString("displayName"));
247 //FIXME(hdq): should read all names
248 builder.withValue(StructuredName.FAMILY_NAME, nameJson.getFirstValue("familyNames"));
249 builder.withValue(StructuredName.GIVEN_NAME, nameJson.getFirstValue("givenNames"));
250 builder.withValue(StructuredName.MIDDLE_NAME, nameJson.getFirstValue("additionalNames"));
251 builder.withValue(StructuredName.PREFIX, nameJson.getFirstValue("honorificPrefixes"));
252 builder.withValue(StructuredName.SUFFIX, nameJson.getFirstValue("honorificSuffixes"));
253 mOps.add(builder.build());
255 // Nickname belongs to another mimetype, so we need another builder for it.
256 if (name.has("nicknames")) {
257 builder = newBuilder(Nickname.CONTENT_ITEM_TYPE);
258 builder.withValue(Nickname.NAME, nameJson.getFirstValue("nicknames"));
259 mOps.add(builder.build());
263 if (mContact.has("categories")) {
264 List<String> groupIds = new ArrayList<String>();
265 for (String groupTitle : mJson.getStringArray("categories")) {
266 groupIds.add(mUtils.getEnsuredGroupId(groupTitle));
268 buildByArray(GroupMembership.CONTENT_ITEM_TYPE, GroupMembership.GROUP_ROW_ID, groupIds);
271 if (mContact.has("gender")) {
272 final String gender = mJson.getString("gender");
273 if (Arrays.asList("male", "female", "other", "none", "unknown").contains(gender)) {
274 builder = newBuilder(ContactConstants.CUSTOM_MIMETYPE_GENDER);
275 builder.withValue(Data.DATA1, gender);
276 mOps.add(builder.build());
280 buildByEvent("birthday", Event.TYPE_BIRTHDAY);
281 buildByEvent("anniversary", Event.TYPE_ANNIVERSARY);
283 buildByContactMapList();
285 // Perform the operation batch
287 mUtils.mResolver.applyBatch(ContactsContract.AUTHORITY, mOps);
288 } catch (Exception e) {
289 if (e instanceof RemoteException ||
290 e instanceof OperationApplicationException ||
291 e instanceof SecurityException) {
292 Log.e(TAG, "Failed to apply batch: " + e.toString());
293 return new JSONObject();
295 throw new RuntimeException(e);
299 // If it is a new contact, need to get and return its auto-generated id.
301 Set<String> newRawIds = mUtils.getCurrentRawIds();
302 if (newRawIds == null) return new JSONObject();
303 newRawIds.removeAll(oldRawIds);
304 if (newRawIds.size() != 1) {
305 Log.e(TAG, "Something wrong after batch applied, "
306 + "new raw ids are: " + newRawIds.toString());
309 mId = mUtils.getId(newRawIds.iterator().next());
310 PutToContact("id", mId);
312 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 ) {
313 PutToContact("lastUpdated", String.valueOf(mUtils.getLastUpdated(Long.valueOf(mId))));