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