Upstream version 8.36.156.0
[platform/framework/web/crosswalk.git] / src / xwalk / runtime / android / core / src / org / xwalk / core / extension / api / contacts / ContactSaver.java
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.
4
5 package org.xwalk.core.extension.api.contacts;
6
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;
20
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26
27 import org.json.JSONArray;
28 import org.json.JSONException;
29 import org.json.JSONObject;
30
31 import org.xwalk.core.extension.api.contacts.ContactConstants.ContactMap;
32
33 /**
34  * This class saves contacts according to a given JSONString.
35  */
36 public class ContactSaver {
37     private ContactUtils mUtils;
38     private static final String TAG = "ContactSaver";
39
40     private JSONObject mContact;
41     private ContactJson mJson;
42     private String mId;
43     private boolean mIsUpdate;
44     private ArrayList<ContentProviderOperation> mOps;
45
46     public ContactSaver(ContentResolver resolver) {
47         mUtils = new ContactUtils(resolver);
48     }
49
50     // Update a contact
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});
56         return builder;
57     }
58
59     // Add a new contact
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);
64         return builder;
65     }
66
67     // Add a new field to an existing contact
68     private Builder newInsertFieldBuilder(String mimeType) {
69         String rawId = mUtils.getRawId(mId);
70         if (rawId == null) {
71             Log.e(TAG, "Failed to create builder to insert field of " + mId);
72             return null;
73         }
74         Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
75         builder.withValue(Data.RAW_CONTACT_ID, mUtils.getRawId(mId));
76         builder.withValue(Data.MIMETYPE, mimeType);
77         return builder;
78     }
79
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);
84     }
85
86     // Add a new contact or update a contact
87     private Builder newBuilder(String mimeType) {
88         return mIsUpdate ? newUpdateBuilder(mimeType)
89                          : newInsertBuilder(mimeType);
90     }
91
92     // Build by a data array with types
93     private void buildByArray(ContactMap contactMap) {
94         if (!mContact.has(contactMap.mName)) {
95             return;
96         }
97
98         // When updating multiple records of one MIMEType,
99         // we need to flush the old records and then insert new ones later.
100         //
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.
109         //
110         if (mIsUpdate) {
111             mUtils.cleanByMimeType(mId, contactMap.mMimeType);
112         }
113         try {
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);
122
123                     Builder builder = newInsertContactOrFieldBuilder(contactMap.mMimeType);
124                     if (builder == null) return;
125
126                     if (json.getBoolean("preferred")) {
127                         builder.withValue(contactMap.mTypeMap.get("isPrimary"), 1);
128                         builder.withValue(contactMap.mTypeMap.get("isSuperPrimary"), 1);
129                     }
130                     if (iType != null) {
131                         builder.withValue(contactMap.mTypeMap.get("type"), iType);
132                     }
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);
143                         }
144                         builder.withValue(entry.getKey(), value);
145                     }
146                     mOps.add(builder.build());
147                 }
148             }
149         } catch (JSONException e) {
150             Log.e(TAG, "Failed to parse json data of " + contactMap.mName + ": " + e.toString());
151         }
152     }
153
154     // Build by a data array without types
155     private void buildByArray(String mimeType, String data, List<String> dataEntries) {
156         if (mIsUpdate) {
157             mUtils.cleanByMimeType(mId, mimeType);
158         }
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());
164         }
165     }
166
167     private void buildByArray(ContactMap contactMap, String data, List<String> dataEntries) {
168         if (mContact.has(contactMap.mName)) {
169             buildByArray(contactMap.mMimeType, data, dataEntries);
170         }
171     }
172
173     private void buildByDate(String name, String mimeType, String data, String type, int dateType) {
174         if (!mContact.has(name)) return;
175
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());
182     }
183
184     private void buildByEvent(String eventName, int eventType) {
185         buildByDate(eventName, Event.CONTENT_ITEM_TYPE, Event.START_DATE, Event.TYPE, eventType);
186     }
187
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));
195             }
196         }
197     }
198
199     private void PutToContact(String name, String value) {
200         if (name == null) return;
201         try {
202             mContact.put(name, value);
203         } catch (JSONException e) {
204             Log.e(TAG, "Failed to set " + name + " = " + value + " for contact" + e.toString());
205         }
206     }
207
208     public JSONObject save(String saveString) {
209         mOps = new ArrayList<ContentProviderOperation>();
210         try {
211             mContact = new JSONObject(saveString);
212         } catch (JSONException e) {
213             Log.e(TAG, "Failed to parse json data: " + e.toString());
214             return new JSONObject();
215         }
216
217         mJson = new ContactJson(mContact);
218
219         Builder builder = null;
220         mId = mJson.getString("id");
221         mIsUpdate = mUtils.hasID(mId);
222
223         Set<String> oldRawIds = null;
224         if (!mIsUpdate) { // Create a null record for inserting later
225             oldRawIds = mUtils.getCurrentRawIds();
226             mId = null;
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());
231         }
232
233         // W3C                  Android
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());
254
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());
260             }
261         }
262
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));
267             }
268             buildByArray(GroupMembership.CONTENT_ITEM_TYPE, GroupMembership.GROUP_ROW_ID, groupIds);
269         }
270
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());
277             }
278         }
279
280         buildByEvent("birthday", Event.TYPE_BIRTHDAY);
281         buildByEvent("anniversary", Event.TYPE_ANNIVERSARY);
282
283         buildByContactMapList();
284
285         // Perform the operation batch
286         try {
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();
294             } else {
295                 throw new RuntimeException(e);
296             }
297         }
298
299         // If it is a new contact, need to get and return its auto-generated id.
300         if (!mIsUpdate) {
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());
307                 return mContact;
308             }
309             mId = mUtils.getId(newRawIds.iterator().next());
310             PutToContact("id", mId);
311         }
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))));
314         }
315         return mContact;
316     }
317 }