1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.providers.contacts;
18 
19 import android.text.TextUtils;
20 import com.android.providers.contacts.util.NeededForTesting;
21 
22 import org.json.JSONArray;
23 import org.json.JSONException;
24 import org.json.JSONObject;
25 
26 import java.util.ArrayList;
27 
28 @NeededForTesting
29 public class MetadataEntryParser {
30 
31     private final static String UNIQUE_CONTACT_ID = "unique_contact_id";
32     private final static String ACCOUNT_TYPE = "account_type";
33     private final static String CUSTOM_ACCOUNT_TYPE = "custom_account_type";
34     private final static String ENUM_VALUE_FOR_GOOGLE_ACCOUNT = "GOOGLE_ACCOUNT";
35     private final static String GOOGLE_ACCOUNT_TYPE = "com.google";
36     private final static String ENUM_VALUE_FOR_CUSTOM_ACCOUNT = "CUSTOM_ACCOUNT";
37     private final static String ACCOUNT_NAME = "account_name";
38     private final static String DATA_SET = "data_set";
39     private final static String ENUM_FOR_PLUS_DATA_SET = "GOOGLE_PLUS";
40     private final static String ENUM_FOR_CUSTOM_DATA_SET = "CUSTOM";
41     private final static String PLUS_DATA_SET_TYPE = "plus";
42     private final static String CUSTOM_DATA_SET = "custom_data_set";
43     private final static String CONTACT_ID = "contact_id";
44     private final static String CONTACT_PREFS = "contact_prefs";
45     private final static String SEND_TO_VOICEMAIL = "send_to_voicemail";
46     private final static String STARRED = "starred";
47     private final static String PINNED = "pinned";
48     private final static String AGGREGATION_DATA = "aggregation_data";
49     private final static String CONTACT_IDS = "contact_ids";
50     private final static String TYPE = "type";
51     private final static String FIELD_DATA = "field_data";
52     private final static String FIELD_DATA_ID = "field_data_id";
53     private final static String FIELD_DATA_PREFS = "field_data_prefs";
54     private final static String IS_PRIMARY = "is_primary";
55     private final static String IS_SUPER_PRIMARY = "is_super_primary";
56     private final static String USAGE_STATS = "usage_stats";
57     private final static String USAGE_TYPE = "usage_type";
58     private final static String LAST_TIME_USED = "last_time_used";
59     private final static String USAGE_COUNT = "usage_count";
60 
61     @NeededForTesting
62     public static class UsageStats {
63         @NeededForTesting
64         final String mUsageType;
65         @NeededForTesting
66         final long mLastTimeUsed;
67         @NeededForTesting
68         final int mTimesUsed;
69 
70         @NeededForTesting
UsageStats(String usageType, long lastTimeUsed, int timesUsed)71         public UsageStats(String usageType, long lastTimeUsed, int timesUsed) {
72             this.mUsageType = usageType;
73             this.mLastTimeUsed = lastTimeUsed;
74             this.mTimesUsed = timesUsed;
75         }
76     }
77 
78     @NeededForTesting
79     public static class FieldData {
80         @NeededForTesting
81         final String mDataHashId;
82         @NeededForTesting
83         final boolean mIsPrimary;
84         @NeededForTesting
85         final boolean mIsSuperPrimary;
86         @NeededForTesting
87         final ArrayList<UsageStats> mUsageStatsList;
88 
89         @NeededForTesting
FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary, ArrayList<UsageStats> usageStatsList)90         public FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary,
91                 ArrayList<UsageStats> usageStatsList) {
92             this.mDataHashId = dataHashId;
93             this.mIsPrimary = isPrimary;
94             this.mIsSuperPrimary = isSuperPrimary;
95             this.mUsageStatsList = usageStatsList;
96         }
97     }
98 
99     @NeededForTesting
100     public static class RawContactInfo {
101         @NeededForTesting
102         final String mBackupId;
103         @NeededForTesting
104         final String mAccountType;
105         @NeededForTesting
106         final String mAccountName;
107         @NeededForTesting
108         final String mDataSet;
109 
110         @NeededForTesting
RawContactInfo(String backupId, String accountType, String accountName, String dataSet)111         public RawContactInfo(String backupId, String accountType, String accountName,
112                 String dataSet) {
113             this.mBackupId = backupId;
114             this.mAccountType = accountType;
115             this.mAccountName = accountName;
116             mDataSet = dataSet;
117         }
118     }
119 
120     @NeededForTesting
121     public static class AggregationData {
122         @NeededForTesting
123         final RawContactInfo mRawContactInfo1;
124         @NeededForTesting
125         final RawContactInfo mRawContactInfo2;
126         @NeededForTesting
127         final String mType;
128 
129         @NeededForTesting
AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2, String type)130         public AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2,
131                 String type) {
132             this.mRawContactInfo1 = rawContactInfo1;
133             this.mRawContactInfo2 = rawContactInfo2;
134             this.mType = type;
135         }
136     }
137 
138     @NeededForTesting
139     public static class MetadataEntry {
140         @NeededForTesting
141         final RawContactInfo mRawContactInfo;
142         @NeededForTesting
143         final int mSendToVoicemail;
144         @NeededForTesting
145         final int mStarred;
146         @NeededForTesting
147         final int mPinned;
148         @NeededForTesting
149         final ArrayList<FieldData> mFieldDatas;
150         @NeededForTesting
151         final ArrayList<AggregationData> mAggregationDatas;
152 
153         @NeededForTesting
MetadataEntry(RawContactInfo rawContactInfo, int sendToVoicemail, int starred, int pinned, ArrayList<FieldData> fieldDatas, ArrayList<AggregationData> aggregationDatas)154         public MetadataEntry(RawContactInfo rawContactInfo,
155                 int sendToVoicemail, int starred, int pinned,
156                 ArrayList<FieldData> fieldDatas,
157                 ArrayList<AggregationData> aggregationDatas) {
158             this.mRawContactInfo = rawContactInfo;
159             this.mSendToVoicemail = sendToVoicemail;
160             this.mStarred = starred;
161             this.mPinned = pinned;
162             this.mFieldDatas = fieldDatas;
163             this.mAggregationDatas = aggregationDatas;
164         }
165     }
166 
167     @NeededForTesting
parseDataToMetaDataEntry(String inputData)168     static MetadataEntry parseDataToMetaDataEntry(String inputData) {
169         if (TextUtils.isEmpty(inputData)) {
170             throw new IllegalArgumentException("Input cannot be empty.");
171         }
172 
173         try {
174             final JSONObject root = new JSONObject(inputData);
175             // Parse to get rawContactId and account info.
176             final JSONObject uniqueContactJSON = root.getJSONObject(UNIQUE_CONTACT_ID);
177             final RawContactInfo rawContactInfo = parseUniqueContact(uniqueContactJSON);
178 
179             // Parse contactPrefs to get sendToVoicemail, starred, pinned.
180             final JSONObject contactPrefs = root.getJSONObject(CONTACT_PREFS);
181             final boolean sendToVoicemail = contactPrefs.has(SEND_TO_VOICEMAIL)
182                     ? contactPrefs.getBoolean(SEND_TO_VOICEMAIL) : false;
183             final boolean starred = contactPrefs.has(STARRED)
184                     ? contactPrefs.getBoolean(STARRED) : false;
185             final int pinned = contactPrefs.has(PINNED) ? contactPrefs.getInt(PINNED) : 0;
186 
187             // Parse aggregationDatas
188             final ArrayList<AggregationData> aggregationsList = new ArrayList<AggregationData>();
189             if (root.has(AGGREGATION_DATA)) {
190                 final JSONArray aggregationDatas = root.getJSONArray(AGGREGATION_DATA);
191 
192                 for (int i = 0; i < aggregationDatas.length(); i++) {
193                     final JSONObject aggregationData = aggregationDatas.getJSONObject(i);
194                     final JSONArray contacts = aggregationData.getJSONArray(CONTACT_IDS);
195 
196                     if (contacts.length() != 2) {
197                         throw new IllegalArgumentException(
198                                 "There should be two contacts for each aggregation.");
199                     }
200                     final JSONObject rawContact1 = contacts.getJSONObject(0);
201                     final RawContactInfo aggregationContact1 = parseUniqueContact(rawContact1);
202                     final JSONObject rawContact2 = contacts.getJSONObject(1);
203                     final RawContactInfo aggregationContact2 = parseUniqueContact(rawContact2);
204                     final String type = aggregationData.getString(TYPE);
205                     if (TextUtils.isEmpty(type)) {
206                         throw new IllegalArgumentException("Aggregation type cannot be empty.");
207                     }
208 
209                     final AggregationData aggregation = new AggregationData(
210                             aggregationContact1, aggregationContact2, type);
211                     aggregationsList.add(aggregation);
212                 }
213             }
214 
215             // Parse fieldDatas
216             final ArrayList<FieldData> fieldDatasList = new ArrayList<FieldData>();
217             if (root.has(FIELD_DATA)) {
218                 final JSONArray fieldDatas = root.getJSONArray(FIELD_DATA);
219 
220                 for (int i = 0; i < fieldDatas.length(); i++) {
221                     final JSONObject fieldData = fieldDatas.getJSONObject(i);
222                     final String dataHashId = fieldData.getString(FIELD_DATA_ID);
223                     if (TextUtils.isEmpty(dataHashId)) {
224                         throw new IllegalArgumentException("Field data hash id cannot be empty.");
225                     }
226                     final JSONObject fieldDataPrefs = fieldData.getJSONObject(FIELD_DATA_PREFS);
227                     final boolean isPrimary = fieldDataPrefs.getBoolean(IS_PRIMARY);
228                     final boolean isSuperPrimary = fieldDataPrefs.getBoolean(IS_SUPER_PRIMARY);
229 
230                     final ArrayList<UsageStats> usageStatsList = new ArrayList<UsageStats>();
231                     if (fieldData.has(USAGE_STATS)) {
232                         final JSONArray usageStats = fieldData.getJSONArray(USAGE_STATS);
233                         for (int j = 0; j < usageStats.length(); j++) {
234                             final JSONObject usageStat = usageStats.getJSONObject(j);
235                             final String usageType = usageStat.getString(USAGE_TYPE);
236                             if (TextUtils.isEmpty(usageType)) {
237                                 throw new IllegalArgumentException("Usage type cannot be empty.");
238                             }
239                             final long lastTimeUsed = usageStat.getLong(LAST_TIME_USED);
240                             final int usageCount = usageStat.getInt(USAGE_COUNT);
241 
242                             final UsageStats usageStatsParsed = new UsageStats(
243                                     usageType, lastTimeUsed, usageCount);
244                             usageStatsList.add(usageStatsParsed);
245                         }
246                     }
247 
248                     final FieldData fieldDataParse = new FieldData(dataHashId, isPrimary,
249                             isSuperPrimary, usageStatsList);
250                     fieldDatasList.add(fieldDataParse);
251                 }
252             }
253             final MetadataEntry metaDataEntry = new MetadataEntry(rawContactInfo,
254                     sendToVoicemail ? 1 : 0, starred ? 1 : 0, pinned,
255                     fieldDatasList, aggregationsList);
256             return metaDataEntry;
257         } catch (JSONException e) {
258             throw new IllegalArgumentException("JSON Exception.", e);
259         }
260     }
261 
parseUniqueContact(JSONObject uniqueContactJSON)262     private static RawContactInfo parseUniqueContact(JSONObject uniqueContactJSON) {
263         try {
264             final String backupId = uniqueContactJSON.getString(CONTACT_ID);
265             final String accountName = uniqueContactJSON.getString(ACCOUNT_NAME);
266             String accountType = uniqueContactJSON.getString(ACCOUNT_TYPE);
267             if (ENUM_VALUE_FOR_GOOGLE_ACCOUNT.equals(accountType)) {
268                 accountType = GOOGLE_ACCOUNT_TYPE;
269             } else if (ENUM_VALUE_FOR_CUSTOM_ACCOUNT.equals(accountType)) {
270                 accountType = uniqueContactJSON.getString(CUSTOM_ACCOUNT_TYPE);
271             } else {
272                 throw new IllegalArgumentException("Unknown account type.");
273             }
274 
275             String dataSet = null;
276             switch (uniqueContactJSON.getString(DATA_SET)) {
277                 case ENUM_FOR_PLUS_DATA_SET:
278                     dataSet = PLUS_DATA_SET_TYPE;
279                     break;
280                 case ENUM_FOR_CUSTOM_DATA_SET:
281                     dataSet = uniqueContactJSON.getString(CUSTOM_DATA_SET);
282                     break;
283             }
284             if (TextUtils.isEmpty(backupId) || TextUtils.isEmpty(accountType)
285                     || TextUtils.isEmpty(accountName)) {
286                 throw new IllegalArgumentException(
287                         "Contact backup id, account type, account name cannot be empty.");
288             }
289             final RawContactInfo rawContactInfo = new RawContactInfo(
290                     backupId, accountType, accountName, dataSet);
291             return rawContactInfo;
292         } catch (JSONException e) {
293             throw new IllegalArgumentException("JSON Exception.", e);
294         }
295     }
296 }
297