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 final String mUsageType; 64 final long mLastTimeUsed; 65 final int mTimesUsed; 66 67 @NeededForTesting 68 public UsageStats(String usageType, long lastTimeUsed, int timesUsed) { 69 this.mUsageType = usageType; 70 this.mLastTimeUsed = lastTimeUsed; 71 this.mTimesUsed = timesUsed; 72 } 73 } 74 75 @NeededForTesting 76 public static class FieldData { 77 final String mDataHashId; 78 final boolean mIsPrimary; 79 final boolean mIsSuperPrimary; 80 final ArrayList<UsageStats> mUsageStatsList; 81 82 @NeededForTesting 83 public FieldData(String dataHashId, boolean isPrimary, boolean isSuperPrimary, 84 ArrayList<UsageStats> usageStatsList) { 85 this.mDataHashId = dataHashId; 86 this.mIsPrimary = isPrimary; 87 this.mIsSuperPrimary = isSuperPrimary; 88 this.mUsageStatsList = usageStatsList; 89 } 90 } 91 92 @NeededForTesting 93 public static class RawContactInfo { 94 final String mBackupId; 95 final String mAccountType; 96 final String mAccountName; 97 final String mDataSet; 98 99 @NeededForTesting 100 public RawContactInfo(String backupId, String accountType, String accountName, 101 String dataSet) { 102 this.mBackupId = backupId; 103 this.mAccountType = accountType; 104 this.mAccountName = accountName; 105 mDataSet = dataSet; 106 } 107 } 108 109 @NeededForTesting 110 public static class AggregationData { 111 final RawContactInfo mRawContactInfo1; 112 final RawContactInfo mRawContactInfo2; 113 final String mType; 114 115 @NeededForTesting 116 public AggregationData(RawContactInfo rawContactInfo1, RawContactInfo rawContactInfo2, 117 String type) { 118 this.mRawContactInfo1 = rawContactInfo1; 119 this.mRawContactInfo2 = rawContactInfo2; 120 this.mType = type; 121 } 122 } 123 124 @NeededForTesting 125 public static class MetadataEntry { 126 final RawContactInfo mRawContactInfo; 127 final int mSendToVoicemail; 128 final int mStarred; 129 final int mPinned; 130 final ArrayList<FieldData> mFieldDatas; 131 final ArrayList<AggregationData> mAggregationDatas; 132 133 @NeededForTesting 134 public MetadataEntry(RawContactInfo rawContactInfo, 135 int sendToVoicemail, int starred, int pinned, 136 ArrayList<FieldData> fieldDatas, 137 ArrayList<AggregationData> aggregationDatas) { 138 this.mRawContactInfo = rawContactInfo; 139 this.mSendToVoicemail = sendToVoicemail; 140 this.mStarred = starred; 141 this.mPinned = pinned; 142 this.mFieldDatas = fieldDatas; 143 this.mAggregationDatas = aggregationDatas; 144 } 145 } 146 147 @NeededForTesting 148 static MetadataEntry parseDataToMetaDataEntry(String inputData) { 149 if (TextUtils.isEmpty(inputData)) { 150 throw new IllegalArgumentException("Input cannot be empty."); 151 } 152 153 try { 154 final JSONObject root = new JSONObject(inputData); 155 // Parse to get rawContactId and account info. 156 final JSONObject uniqueContactJSON = root.getJSONObject(UNIQUE_CONTACT_ID); 157 final RawContactInfo rawContactInfo = parseUniqueContact(uniqueContactJSON); 158 159 // Parse contactPrefs to get sendToVoicemail, starred, pinned. 160 final JSONObject contactPrefs = root.getJSONObject(CONTACT_PREFS); 161 final boolean sendToVoicemail = contactPrefs.has(SEND_TO_VOICEMAIL) 162 ? contactPrefs.getBoolean(SEND_TO_VOICEMAIL) : false; 163 final boolean starred = contactPrefs.has(STARRED) 164 ? contactPrefs.getBoolean(STARRED) : false; 165 final int pinned = contactPrefs.has(PINNED) ? contactPrefs.getInt(PINNED) : 0; 166 167 // Parse aggregationDatas 168 final ArrayList<AggregationData> aggregationsList = new ArrayList<AggregationData>(); 169 if (root.has(AGGREGATION_DATA)) { 170 final JSONArray aggregationDatas = root.getJSONArray(AGGREGATION_DATA); 171 172 for (int i = 0; i < aggregationDatas.length(); i++) { 173 final JSONObject aggregationData = aggregationDatas.getJSONObject(i); 174 final JSONArray contacts = aggregationData.getJSONArray(CONTACT_IDS); 175 176 if (contacts.length() != 2) { 177 throw new IllegalArgumentException( 178 "There should be two contacts for each aggregation."); 179 } 180 final JSONObject rawContact1 = contacts.getJSONObject(0); 181 final RawContactInfo aggregationContact1 = parseUniqueContact(rawContact1); 182 final JSONObject rawContact2 = contacts.getJSONObject(1); 183 final RawContactInfo aggregationContact2 = parseUniqueContact(rawContact2); 184 final String type = aggregationData.getString(TYPE); 185 if (TextUtils.isEmpty(type)) { 186 throw new IllegalArgumentException("Aggregation type cannot be empty."); 187 } 188 189 final AggregationData aggregation = new AggregationData( 190 aggregationContact1, aggregationContact2, type); 191 aggregationsList.add(aggregation); 192 } 193 } 194 195 // Parse fieldDatas 196 final ArrayList<FieldData> fieldDatasList = new ArrayList<FieldData>(); 197 if (root.has(FIELD_DATA)) { 198 final JSONArray fieldDatas = root.getJSONArray(FIELD_DATA); 199 200 for (int i = 0; i < fieldDatas.length(); i++) { 201 final JSONObject fieldData = fieldDatas.getJSONObject(i); 202 final String dataHashId = fieldData.getString(FIELD_DATA_ID); 203 if (TextUtils.isEmpty(dataHashId)) { 204 throw new IllegalArgumentException("Field data hash id cannot be empty."); 205 } 206 final JSONObject fieldDataPrefs = fieldData.getJSONObject(FIELD_DATA_PREFS); 207 final boolean isPrimary = fieldDataPrefs.getBoolean(IS_PRIMARY); 208 final boolean isSuperPrimary = fieldDataPrefs.getBoolean(IS_SUPER_PRIMARY); 209 210 final ArrayList<UsageStats> usageStatsList = new ArrayList<UsageStats>(); 211 if (fieldData.has(USAGE_STATS)) { 212 final JSONArray usageStats = fieldData.getJSONArray(USAGE_STATS); 213 for (int j = 0; j < usageStats.length(); j++) { 214 final JSONObject usageStat = usageStats.getJSONObject(j); 215 final String usageType = usageStat.getString(USAGE_TYPE); 216 if (TextUtils.isEmpty(usageType)) { 217 throw new IllegalArgumentException("Usage type cannot be empty."); 218 } 219 final long lastTimeUsed = usageStat.getLong(LAST_TIME_USED); 220 final int usageCount = usageStat.getInt(USAGE_COUNT); 221 222 final UsageStats usageStatsParsed = new UsageStats( 223 usageType, lastTimeUsed, usageCount); 224 usageStatsList.add(usageStatsParsed); 225 } 226 } 227 228 final FieldData fieldDataParse = new FieldData(dataHashId, isPrimary, 229 isSuperPrimary, usageStatsList); 230 fieldDatasList.add(fieldDataParse); 231 } 232 } 233 final MetadataEntry metaDataEntry = new MetadataEntry(rawContactInfo, 234 sendToVoicemail ? 1 : 0, starred ? 1 : 0, pinned, 235 fieldDatasList, aggregationsList); 236 return metaDataEntry; 237 } catch (JSONException e) { 238 throw new IllegalArgumentException("JSON Exception.", e); 239 } 240 } 241 242 private static RawContactInfo parseUniqueContact(JSONObject uniqueContactJSON) { 243 try { 244 final String backupId = uniqueContactJSON.getString(CONTACT_ID); 245 final String accountName = uniqueContactJSON.getString(ACCOUNT_NAME); 246 String accountType = uniqueContactJSON.getString(ACCOUNT_TYPE); 247 if (ENUM_VALUE_FOR_GOOGLE_ACCOUNT.equals(accountType)) { 248 accountType = GOOGLE_ACCOUNT_TYPE; 249 } else if (ENUM_VALUE_FOR_CUSTOM_ACCOUNT.equals(accountType)) { 250 accountType = uniqueContactJSON.getString(CUSTOM_ACCOUNT_TYPE); 251 } else { 252 throw new IllegalArgumentException("Unknown account type."); 253 } 254 255 String dataSet = null; 256 switch (uniqueContactJSON.getString(DATA_SET)) { 257 case ENUM_FOR_PLUS_DATA_SET: 258 dataSet = PLUS_DATA_SET_TYPE; 259 break; 260 case ENUM_FOR_CUSTOM_DATA_SET: 261 dataSet = uniqueContactJSON.getString(CUSTOM_DATA_SET); 262 break; 263 } 264 if (TextUtils.isEmpty(backupId) || TextUtils.isEmpty(accountType) 265 || TextUtils.isEmpty(accountName)) { 266 throw new IllegalArgumentException( 267 "Contact backup id, account type, account name cannot be empty."); 268 } 269 final RawContactInfo rawContactInfo = new RawContactInfo( 270 backupId, accountType, accountName, dataSet); 271 return rawContactInfo; 272 } catch (JSONException e) { 273 throw new IllegalArgumentException("JSON Exception.", e); 274 } 275 } 276 } 277