blob: 540bf1a7488d710a1338225c020311cab5c36881 [file] [log] [blame]
Walter Jangcab3dce2015-02-09 17:48:03 -08001/*
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
17package com.android.contacts.editor;
18
19import com.android.contacts.R;
20import com.android.contacts.common.model.AccountTypeManager;
21import com.android.contacts.common.model.RawContactDelta;
22import com.android.contacts.common.model.RawContactDeltaList;
Walter Jange720fde2015-02-17 10:54:14 -080023import com.android.contacts.common.model.RawContactModifier;
Walter Jangcab3dce2015-02-09 17:48:03 -080024import com.android.contacts.common.model.ValuesDelta;
25import com.android.contacts.common.model.account.AccountType;
Walter Jang2d3f31c2015-06-18 23:15:31 -070026import com.android.contacts.common.model.account.AccountWithDataSet;
Walter Jangcab3dce2015-02-09 17:48:03 -080027import com.android.contacts.common.model.dataitem.DataKind;
Walter Jang708ea9e2015-09-10 15:42:05 -070028import com.android.contacts.common.util.AccountsListAdapter;
Walter Jangf46abd82015-02-20 16:52:04 -080029import com.android.contacts.common.util.MaterialColorMapUtils;
Walter Jang3efae4a2015-02-18 11:12:00 -080030import com.android.contacts.editor.CompactContactEditorFragment.PhotoHandler;
Walter Jang708ea9e2015-09-10 15:42:05 -070031import com.android.contacts.util.UiClosables;
Walter Jangcab3dce2015-02-09 17:48:03 -080032
33import android.content.Context;
Walter Jang3efae4a2015-02-18 11:12:00 -080034import android.graphics.Bitmap;
Walter Jang41b3ea12015-03-09 17:30:06 -070035import android.net.Uri;
Walter Jangcab3dce2015-02-09 17:48:03 -080036import android.provider.ContactsContract.CommonDataKinds.Email;
Walter Jangf5dfea42015-09-16 12:30:36 -070037import android.provider.ContactsContract.CommonDataKinds.Event;
Walter Jangcab3dce2015-02-09 17:48:03 -080038import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Walter Jangf5dfea42015-09-16 12:30:36 -070039import android.provider.ContactsContract.CommonDataKinds.Im;
Walter Jang3efae4a2015-02-18 11:12:00 -080040import android.provider.ContactsContract.CommonDataKinds.Nickname;
Walter Jangf5dfea42015-09-16 12:30:36 -070041import android.provider.ContactsContract.CommonDataKinds.Note;
42import android.provider.ContactsContract.CommonDataKinds.Organization;
Walter Jangcab3dce2015-02-09 17:48:03 -080043import android.provider.ContactsContract.CommonDataKinds.Phone;
44import android.provider.ContactsContract.CommonDataKinds.Photo;
Walter Jangf5dfea42015-09-16 12:30:36 -070045import android.provider.ContactsContract.CommonDataKinds.Relation;
46import android.provider.ContactsContract.CommonDataKinds.SipAddress;
Walter Jangcab3dce2015-02-09 17:48:03 -080047import android.provider.ContactsContract.CommonDataKinds.StructuredName;
Walter Jangf5dfea42015-09-16 12:30:36 -070048import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
49import android.provider.ContactsContract.CommonDataKinds.Website;
Walter Jangcab3dce2015-02-09 17:48:03 -080050import android.text.TextUtils;
51import android.util.AttributeSet;
52import android.util.Log;
Walter Jang2d3f31c2015-06-18 23:15:31 -070053import android.util.Pair;
Walter Jangcab3dce2015-02-09 17:48:03 -080054import android.view.LayoutInflater;
Walter Jangb1c87622015-02-13 17:51:38 -080055import android.view.View;
Walter Jangcab3dce2015-02-09 17:48:03 -080056import android.view.ViewGroup;
Walter Jang708ea9e2015-09-10 15:42:05 -070057import android.widget.AdapterView;
Walter Jangcab3dce2015-02-09 17:48:03 -080058import android.widget.LinearLayout;
Walter Jang708ea9e2015-09-10 15:42:05 -070059import android.widget.ListPopupWindow;
Walter Jang2d3f31c2015-06-18 23:15:31 -070060import android.widget.TextView;
Walter Jangcab3dce2015-02-09 17:48:03 -080061
62import java.util.ArrayList;
Walter Jangf5dfea42015-09-16 12:30:36 -070063import java.util.Arrays;
Walter Jang192a01c2015-09-22 15:23:55 -070064import java.util.Collections;
Walter Jangf5dfea42015-09-16 12:30:36 -070065import java.util.Comparator;
Walter Jang19d985f2015-07-01 13:36:27 -070066import java.util.HashMap;
Walter Jangcab3dce2015-02-09 17:48:03 -080067import java.util.List;
Walter Jang19d985f2015-07-01 13:36:27 -070068import java.util.Map;
Walter Jang708ea9e2015-09-10 15:42:05 -070069import java.util.Objects;
Walter Jangf5dfea42015-09-16 12:30:36 -070070import java.util.TreeSet;
Walter Jangcab3dce2015-02-09 17:48:03 -080071
72/**
Walter Jangf5dfea42015-09-16 12:30:36 -070073 * View to display information from multiple {@link RawContactDelta}s grouped together.
Walter Jangcab3dce2015-02-09 17:48:03 -080074 */
Walter Jangb6ca2722015-02-20 11:10:25 -080075public class CompactRawContactsEditorView extends LinearLayout implements View.OnClickListener {
Walter Jangcab3dce2015-02-09 17:48:03 -080076
77 private static final String TAG = "CompactEditorView";
78
Walter Jang192a01c2015-09-22 15:23:55 -070079 private static final KindSectionDataMapEntryComparator
80 KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR = new KindSectionDataMapEntryComparator();
Walter Jangf5dfea42015-09-16 12:30:36 -070081
Walter Jangb6ca2722015-02-20 11:10:25 -080082 /**
83 * Callbacks for hosts of {@link CompactRawContactsEditorView}s.
84 */
85 public interface Listener {
86
87 /**
Walter Jang151f3e62015-02-26 15:29:40 -080088 * Invoked when the structured name editor field has changed.
89 *
90 * @param rawContactId The raw contact ID from the underlying {@link RawContactDelta}.
91 * @param valuesDelta The values from the underlying {@link RawContactDelta}.
92 */
93 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta);
Walter Jang708ea9e2015-09-10 15:42:05 -070094
95 /**
96 * Invoked when the compact editor should rebind editors for a new account.
97 *
98 * @param oldState Old data being edited.
99 * @param oldAccount Old account associated with oldState.
100 * @param newAccount New account to be used.
101 */
102 public void onRebindEditorsForNewContact(RawContactDelta oldState,
103 AccountWithDataSet oldAccount, AccountWithDataSet newAccount);
Walter Jang151f3e62015-02-26 15:29:40 -0800104 }
105
Walter Jang192a01c2015-09-22 15:23:55 -0700106 /** Used to sort entire kind sections. */
107 private static final class KindSectionDataMapEntryComparator implements
Walter Jangf5dfea42015-09-16 12:30:36 -0700108 Comparator<Map.Entry<String,List<KindSectionData>>> {
109
Walter Jang192a01c2015-09-22 15:23:55 -0700110 final MimeTypeComparator mMimeTypeComparator = new MimeTypeComparator();
111
112 @Override
113 public int compare(Map.Entry<String, List<KindSectionData>> entry1,
114 Map.Entry<String, List<KindSectionData>> entry2) {
115 if (entry1 == entry2) return 0;
116 if (entry1 == null) return -1;
117 if (entry2 == null) return 1;
118
119 final String mimeType1 = entry1.getKey();
120 final String mimeType2 = entry2.getKey();
121
122 return mMimeTypeComparator.compare(mimeType1, mimeType2);
123 }
124 }
125
126 /**
127 * Sorts kinds roughly the same as quick contacts; we diverge in the following ways:
128 * <ol>
129 * <li>All names are together at the top.</li>
130 * <li>IM is moved up after addresses</li>
131 * <li>SIP addresses are moved to below phone numbers</li>
132 * </ol>
133 */
134 private static final class MimeTypeComparator implements Comparator<String> {
135
Walter Jangf5dfea42015-09-16 12:30:36 -0700136 private static final List<String> MIME_TYPE_ORDER = Arrays.asList(new String[] {
137 StructuredName.CONTENT_ITEM_TYPE,
Walter Jangf5dfea42015-09-16 12:30:36 -0700138 Nickname.CONTENT_ITEM_TYPE,
139 Phone.CONTENT_ITEM_TYPE,
140 SipAddress.CONTENT_ITEM_TYPE,
141 Email.CONTENT_ITEM_TYPE,
142 StructuredPostal.CONTENT_ITEM_TYPE,
143 Im.CONTENT_ITEM_TYPE,
144 Website.CONTENT_ITEM_TYPE,
145 Organization.CONTENT_ITEM_TYPE,
146 Event.CONTENT_ITEM_TYPE,
147 Relation.CONTENT_ITEM_TYPE,
148 Note.CONTENT_ITEM_TYPE
149 });
150
151 @Override
Walter Jang192a01c2015-09-22 15:23:55 -0700152 public int compare(String mimeType1, String mimeType2) {
153 if (mimeType1 == mimeType2) return 0;
154 if (mimeType1 == null) return -1;
155 if (mimeType2 == null) return 1;
Walter Jangf5dfea42015-09-16 12:30:36 -0700156
157 int index1 = MIME_TYPE_ORDER.indexOf(mimeType1);
158 int index2 = MIME_TYPE_ORDER.indexOf(mimeType2);
159
160 // Fallback to alphabetical ordering of the mime type if both are not found
161 if (index1 < 0 && index2 < 0) return mimeType1.compareTo(mimeType2);
162 if (index1 < 0) return 1;
163 if (index2 < 0) return -1;
164
165 return index1 < index2 ? -1 : 1;
166 }
167 }
168
Walter Jang192a01c2015-09-22 15:23:55 -0700169 /**
170 * Sorts primary accounts and google account types before others.
171 */
172 private static final class EditorComparator implements Comparator<KindSectionData> {
173
174 private RawContactDeltaComparator mRawContactDeltaComparator;
175
176 private EditorComparator(Context context) {
177 mRawContactDeltaComparator = new RawContactDeltaComparator(context);
178 }
179
180 @Override
181 public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) {
182 if (kindSectionData1 == kindSectionData2) return 0;
183 if (kindSectionData1 == null) return -1;
184 if (kindSectionData2 == null) return 1;
185
186 final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta();
187 final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta();
188
189 if (rawContactDelta1 == rawContactDelta2) return 0;
190 if (rawContactDelta1 == null) return -1;
191 if (rawContactDelta2 == null) return 1;
192
193 return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2);
194 }
195 }
196
197 /**
198 * Sorts primary account names first, followed by google account types, and other account
199 * types last. For names from the same account we order structured names before nicknames,
200 * but still keep names from the same account together.
201 */
202 private static final class NameEditorComparator implements Comparator<KindSectionData> {
203
204 private RawContactDeltaComparator mRawContactDeltaComparator;
205 private MimeTypeComparator mMimeTypeComparator;
206 private RawContactDelta mPrimaryRawContactDelta;
207
208 private NameEditorComparator(Context context, RawContactDelta primaryRawContactDelta) {
209 mRawContactDeltaComparator = new RawContactDeltaComparator(context);
210 mMimeTypeComparator = new MimeTypeComparator();
211 mPrimaryRawContactDelta = primaryRawContactDelta;
212 }
213
214 @Override
215 public int compare(KindSectionData kindSectionData1, KindSectionData kindSectionData2) {
216 if (kindSectionData1 == kindSectionData2) return 0;
217 if (kindSectionData1 == null) return -1;
218 if (kindSectionData2 == null) return 1;
219
220 final RawContactDelta rawContactDelta1 = kindSectionData1.getRawContactDelta();
221 final RawContactDelta rawContactDelta2 = kindSectionData2.getRawContactDelta();
222
223 if (rawContactDelta1 == rawContactDelta2) return 0;
224 if (rawContactDelta1 == null) return -1;
225 if (rawContactDelta2 == null) return 1;
226
227 final boolean isRawContactDelta1Primary =
228 mPrimaryRawContactDelta.equals(rawContactDelta1);
229 final boolean isRawContactDelta2Primary =
230 mPrimaryRawContactDelta.equals(rawContactDelta2);
231
232 // If both names are from the primary account, sort my by mime type
233 if (isRawContactDelta1Primary && isRawContactDelta2Primary) {
234 final String mimeType1 = kindSectionData1.getDataKind().mimeType;
235 final String mimeType2 = kindSectionData2.getDataKind().mimeType;
236 return mMimeTypeComparator.compare(mimeType1, mimeType2);
237 }
238
239 // The primary account name should be before all others
240 if (isRawContactDelta1Primary) return 1;
241 if (isRawContactDelta2Primary) return -1;
242
243 return mRawContactDeltaComparator.compare(rawContactDelta1, rawContactDelta2);
244 }
245 }
246
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700247 private CompactRawContactsEditorView.Listener mListener;
Walter Jangb6ca2722015-02-20 11:10:25 -0800248
Walter Jangcab3dce2015-02-09 17:48:03 -0800249 private AccountTypeManager mAccountTypeManager;
250 private LayoutInflater mLayoutInflater;
Walter Jangd35e5ef2015-02-24 09:18:16 -0800251
Walter Jangcab3dce2015-02-09 17:48:03 -0800252 private ViewIdGenerator mViewIdGenerator;
Walter Jangf46abd82015-02-20 16:52:04 -0800253 private MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang708ea9e2015-09-10 15:42:05 -0700254 private long mPhotoId;
Walter Jang708ea9e2015-09-10 15:42:05 -0700255 private boolean mHasNewContact;
256 private boolean mIsUserProfile;
257 private AccountWithDataSet mPrimaryAccount;
258 private RawContactDelta mPrimaryRawContactDelta;
Walter Jang363d3fd2015-09-16 10:29:07 -0700259 private Map<String,List<KindSectionData>> mKindSectionDataMap = new HashMap<>();
Walter Jangcab3dce2015-02-09 17:48:03 -0800260
Walter Jang708ea9e2015-09-10 15:42:05 -0700261 // Account header
262 private View mAccountHeaderContainer;
263 private TextView mAccountHeaderType;
264 private TextView mAccountHeaderName;
265
266 // Account selector
267 private View mAccountSelectorContainer;
268 private View mAccountSelector;
269 private TextView mAccountSelectorType;
270 private TextView mAccountSelectorName;
271
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700272 private CompactPhotoEditorView mPhotoView;
Walter Jangf5dfea42015-09-16 12:30:36 -0700273 private ViewGroup mKindSectionViews;
Walter Jangb6ca2722015-02-20 11:10:25 -0800274 private View mMoreFields;
Walter Jangcab3dce2015-02-09 17:48:03 -0800275
Walter Jang3efae4a2015-02-18 11:12:00 -0800276 private long mPhotoRawContactId;
277
Walter Jangcab3dce2015-02-09 17:48:03 -0800278 public CompactRawContactsEditorView(Context context) {
279 super(context);
280 }
281
282 public CompactRawContactsEditorView(Context context, AttributeSet attrs) {
283 super(context, attrs);
284 }
285
Walter Jangb6ca2722015-02-20 11:10:25 -0800286 /**
287 * Sets the receiver for {@link CompactRawContactsEditorView} callbacks.
288 */
289 public void setListener(Listener listener) {
290 mListener = listener;
291 }
292
Walter Jangcab3dce2015-02-09 17:48:03 -0800293 @Override
294 protected void onFinishInflate() {
295 super.onFinishInflate();
296
297 mAccountTypeManager = AccountTypeManager.getInstance(getContext());
298 mLayoutInflater = (LayoutInflater)
299 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
300
Walter Jang708ea9e2015-09-10 15:42:05 -0700301 // Account header
302 mAccountHeaderContainer = findViewById(R.id.account_container);
303 mAccountHeaderType = (TextView) findViewById(R.id.account_type);
304 mAccountHeaderName = (TextView) findViewById(R.id.account_name);
305
306 // Account selector
307 mAccountSelectorContainer = findViewById(R.id.account_selector_container);
308 mAccountSelector = findViewById(R.id.account);
309 mAccountSelectorType = (TextView) findViewById(R.id.account_type_selector);
310 mAccountSelectorName = (TextView) findViewById(R.id.account_name_selector);
311
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700312 mPhotoView = (CompactPhotoEditorView) findViewById(R.id.photo_editor);
Walter Jangf5dfea42015-09-16 12:30:36 -0700313 mKindSectionViews = (LinearLayout) findViewById(R.id.kind_section_views);
Walter Jangb6ca2722015-02-20 11:10:25 -0800314 mMoreFields = findViewById(R.id.more_fields);
315 mMoreFields.setOnClickListener(this);
316 }
317
Walter Jangb6ca2722015-02-20 11:10:25 -0800318 @Override
319 public void onClick(View view) {
Walter Jangf5dfea42015-09-16 12:30:36 -0700320 if (view.getId() == R.id.more_fields) {
321 // Stop hiding empty editors and allow the user to enter values for all kinds now
322 for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
323 final CompactKindSectionView kindSectionView =
324 (CompactKindSectionView) mKindSectionViews.getChildAt(i);
325 kindSectionView.setHideWhenEmpty(false);
Walter Jang192a01c2015-09-22 15:23:55 -0700326 // Except the user is never allowed to add new names
327 final String mimeType = kindSectionView.getMimeType();
328 if (!StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700329 kindSectionView.setShowOneEmptyEditor(true);
330 }
Walter Jangf5dfea42015-09-16 12:30:36 -0700331 kindSectionView.updateEmptyEditors(/* shouldAnimate =*/ false);
332 }
333
334 updateMoreFieldsButton();
Walter Jangb6ca2722015-02-20 11:10:25 -0800335 }
Walter Jangcab3dce2015-02-09 17:48:03 -0800336 }
337
338 @Override
339 public void setEnabled(boolean enabled) {
340 super.setEnabled(enabled);
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700341 final int childCount = mKindSectionViews.getChildCount();
342 for (int i = 0; i < childCount; i++) {
343 mKindSectionViews.getChildAt(i).setEnabled(enabled);
Walter Jangcab3dce2015-02-09 17:48:03 -0800344 }
345 }
346
Walter Jang3efae4a2015-02-18 11:12:00 -0800347 /**
Walter Janga5e4bb22015-02-24 13:08:16 -0800348 * Pass through to {@link CompactPhotoEditorView#setPhotoHandler}.
Walter Jang3efae4a2015-02-18 11:12:00 -0800349 */
350 public void setPhotoHandler(PhotoHandler photoHandler) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700351 mPhotoView.setPhotoHandler(photoHandler);
Walter Jang3efae4a2015-02-18 11:12:00 -0800352 }
353
354 /**
Walter Janga5e4bb22015-02-24 13:08:16 -0800355 * Pass through to {@link CompactPhotoEditorView#setPhoto}.
Walter Jang3efae4a2015-02-18 11:12:00 -0800356 */
357 public void setPhoto(Bitmap bitmap) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700358 mPhotoView.setPhoto(bitmap);
Walter Jang3efae4a2015-02-18 11:12:00 -0800359 }
360
361 /**
Walter Jang41b3ea12015-03-09 17:30:06 -0700362 * Pass through to {@link CompactPhotoEditorView#setFullSizedPhoto(Uri)}.
363 */
364 public void setFullSizePhoto(Uri photoUri) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700365 mPhotoView.setFullSizedPhoto(photoUri);
Walter Jang41b3ea12015-03-09 17:30:06 -0700366 }
367
368 /**
Walter Janga5e4bb22015-02-24 13:08:16 -0800369 * Pass through to {@link CompactPhotoEditorView#isWritablePhotoSet}.
Walter Jang3efae4a2015-02-18 11:12:00 -0800370 */
371 public boolean isWritablePhotoSet() {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700372 return mPhotoView.isWritablePhotoSet();
Walter Jang3efae4a2015-02-18 11:12:00 -0800373 }
374
375 /**
Walter Jang3efae4a2015-02-18 11:12:00 -0800376 * Get the raw contact ID for the CompactHeaderView photo.
377 */
Walter Jang3efae4a2015-02-18 11:12:00 -0800378 public long getPhotoRawContactId() {
379 return mPhotoRawContactId;
380 }
381
Walter Jangd35e5ef2015-02-24 09:18:16 -0800382 public View getAggregationAnchorView() {
Walter Jang192a01c2015-09-22 15:23:55 -0700383 // The kind section for the primary account is sorted to the front
384 final List<KindSectionData> kindSectionDataList = getKindSectionDataList(
385 StructuredName.CONTENT_ITEM_TYPE);
386 if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) {
387 return mKindSectionViews.getChildAt(0).findViewById(R.id.anchor_view);
Walter Jangd35e5ef2015-02-24 09:18:16 -0800388 }
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700389 return null;
Walter Jangd35e5ef2015-02-24 09:18:16 -0800390 }
391
Walter Jangf46abd82015-02-20 16:52:04 -0800392 public void setState(RawContactDeltaList rawContactDeltas,
Walter Jang06f73a12015-06-17 11:15:48 -0700393 MaterialColorMapUtils.MaterialPalette materialPalette, ViewIdGenerator viewIdGenerator,
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700394 long photoId, boolean hasNewContact, boolean isUserProfile,
395 AccountWithDataSet primaryAccount) {
396 // Clear previous state and reset views
Walter Jang363d3fd2015-09-16 10:29:07 -0700397 mKindSectionDataMap.clear();
Walter Jangf5dfea42015-09-16 12:30:36 -0700398 mKindSectionViews.removeAllViews();
Walter Jang363d3fd2015-09-16 10:29:07 -0700399 mMoreFields.setVisibility(View.VISIBLE);
Walter Jangcab3dce2015-02-09 17:48:03 -0800400
Walter Jangf46abd82015-02-20 16:52:04 -0800401 mMaterialPalette = materialPalette;
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700402 mViewIdGenerator = viewIdGenerator;
Walter Jang708ea9e2015-09-10 15:42:05 -0700403 mPhotoId = photoId;
Walter Jang708ea9e2015-09-10 15:42:05 -0700404 mHasNewContact = hasNewContact;
405 mIsUserProfile = isUserProfile;
406 mPrimaryAccount = primaryAccount;
407 if (mPrimaryAccount == null) {
408 mPrimaryAccount = ContactEditorUtils.getInstance(getContext()).getDefaultAccount();
409 }
410 vlog("state: primary " + mPrimaryAccount);
Walter Jangcab3dce2015-02-09 17:48:03 -0800411
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700412 // Parse the given raw contact deltas
413 if (rawContactDeltas == null || rawContactDeltas.isEmpty()) {
414 elog("No raw contact deltas");
Walter Jangf5dfea42015-09-16 12:30:36 -0700415 return;
416 }
Walter Jang192a01c2015-09-22 15:23:55 -0700417 parseRawContactDeltas(rawContactDeltas, mPrimaryAccount);
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700418 if (mKindSectionDataMap == null || mKindSectionDataMap.isEmpty()) {
419 elog("No kind section data parsed from RawContactDelta(s)");
420 return;
421 }
422
423 // Setup the view
424 setId(mViewIdGenerator.getId(rawContactDeltas.get(0), /* dataKind =*/ null,
425 /* valuesDelta =*/ null, ViewIdGenerator.NO_VIEW_INDEX));
Walter Jang708ea9e2015-09-10 15:42:05 -0700426 addAccountInfo();
Walter Jang363d3fd2015-09-16 10:29:07 -0700427 addPhotoView();
Walter Jangf5dfea42015-09-16 12:30:36 -0700428 addKindSectionViews();
Walter Jangf5dfea42015-09-16 12:30:36 -0700429 updateMoreFieldsButton();
Walter Jangcab3dce2015-02-09 17:48:03 -0800430 }
431
Walter Jang192a01c2015-09-22 15:23:55 -0700432 private void parseRawContactDeltas(RawContactDeltaList rawContactDeltas,
433 AccountWithDataSet primaryAccount) {
434 if (primaryAccount != null) {
Walter Jang708ea9e2015-09-10 15:42:05 -0700435 // Use the first writable contact that matches the primary account
436 for (RawContactDelta rawContactDelta : rawContactDeltas) {
437 if (!rawContactDelta.isVisible()) continue;
438 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700439 if (accountType == null || !accountType.areContactsWritable()) continue;
Walter Jang192a01c2015-09-22 15:23:55 -0700440 if (matchesAccount(primaryAccount, rawContactDelta)) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700441 vlog("parse: matched primary account raw contact");
Walter Jang708ea9e2015-09-10 15:42:05 -0700442 mPrimaryRawContactDelta = rawContactDelta;
443 break;
444 }
Walter Jang2d3f31c2015-06-18 23:15:31 -0700445 }
446 }
Walter Jang708ea9e2015-09-10 15:42:05 -0700447 if (mPrimaryRawContactDelta == null) {
448 // Fall back to the first writable raw contact
449 for (RawContactDelta rawContactDelta : rawContactDeltas) {
450 if (!rawContactDelta.isVisible()) continue;
451 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
452 if (accountType != null && accountType.areContactsWritable()) {
453 vlog("parse: falling back to the first writable raw contact as primary");
454 mPrimaryRawContactDelta = rawContactDelta;
455 break;
456 }
457 }
Walter Jang2d3f31c2015-06-18 23:15:31 -0700458 }
Walter Jang192a01c2015-09-22 15:23:55 -0700459
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700460 if (mPrimaryRawContactDelta != null) {
461 RawContactModifier.ensureKindExists(mPrimaryRawContactDelta,
462 mPrimaryRawContactDelta.getAccountType(mAccountTypeManager),
463 StructuredName.CONTENT_ITEM_TYPE);
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700464 RawContactModifier.ensureKindExists(mPrimaryRawContactDelta,
465 mPrimaryRawContactDelta.getAccountType(mAccountTypeManager),
466 Photo.CONTENT_ITEM_TYPE);
467 }
Walter Jangfa127a12015-06-18 09:48:18 -0700468
Walter Jang363d3fd2015-09-16 10:29:07 -0700469 // Build the kind section data list map
Walter Jang192a01c2015-09-22 15:23:55 -0700470 vlog("parse: " + rawContactDeltas.size() + " rawContactDelta(s)");
471 for (int j = 0; j < rawContactDeltas.size(); j++) {
472 final RawContactDelta rawContactDelta = rawContactDeltas.get(j);
473 vlog("parse: " + j + " rawContactDelta" + rawContactDelta);
Walter Jang363d3fd2015-09-16 10:29:07 -0700474 if (rawContactDelta == null || !rawContactDelta.isVisible()) continue;
Walter Jangcab3dce2015-02-09 17:48:03 -0800475 final AccountType accountType = rawContactDelta.getAccountType(mAccountTypeManager);
Walter Jang363d3fd2015-09-16 10:29:07 -0700476 if (accountType == null) continue;
477 final List<DataKind> dataKinds = accountType.getSortedDataKinds();
478 final int dataKindSize = dataKinds == null ? 0 : dataKinds.size();
479 vlog("parse: " + dataKindSize + " dataKinds(s)");
480 for (int i = 0; i < dataKindSize; i++) {
481 final DataKind dataKind = dataKinds.get(i);
Walter Jang192a01c2015-09-22 15:23:55 -0700482 if (dataKind == null || !dataKind.editable) {
483 vlog("parse: " + i + " " + dataKind.mimeType + " dropped read-only");
484 continue;
485 }
486 final String mimeType = dataKind.mimeType;
487
488 // Skip psuedo mime types
489 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
490 || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
491 vlog("parse: " + i + " " + dataKind.mimeType + " dropped pseudo type");
492 continue;
493 }
494
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700495 final List<KindSectionData> kindSectionDataList =
Walter Jang192a01c2015-09-22 15:23:55 -0700496 getKindSectionDataList(mimeType);
Walter Jang363d3fd2015-09-16 10:29:07 -0700497 final KindSectionData kindSectionData =
498 new KindSectionData(accountType, dataKind, rawContactDelta);
499 kindSectionDataList.add(kindSectionData);
Walter Jang192a01c2015-09-22 15:23:55 -0700500
501 // Note we must create a nickname entry on inserts
502 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)
503 && kindSectionData.getValuesDeltas().isEmpty()
504 && mHasNewContact) {
505 RawContactModifier.insertChild(rawContactDelta, dataKind);
506 }
507
Walter Jang363d3fd2015-09-16 10:29:07 -0700508 vlog("parse: " + i + " " + dataKind.mimeType + " " +
Walter Jang192a01c2015-09-22 15:23:55 -0700509 kindSectionData.getValuesDeltas().size() + " value(s)");
Walter Jangac679af2015-06-01 12:17:06 -0700510 }
511 }
Walter Jang3efae4a2015-02-18 11:12:00 -0800512 }
513
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700514 private List<KindSectionData> getKindSectionDataList(String mimeType) {
Walter Jang192a01c2015-09-22 15:23:55 -0700515 // Put structured names and nicknames together
516 mimeType = Nickname.CONTENT_ITEM_TYPE.equals(mimeType)
517 ? StructuredName.CONTENT_ITEM_TYPE : mimeType;
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700518 List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
519 if (kindSectionDataList == null) {
520 kindSectionDataList = new ArrayList<>();
521 mKindSectionDataMap.put(mimeType, kindSectionDataList);
522 }
523 return kindSectionDataList;
524 }
525
Walter Jang192a01c2015-09-22 15:23:55 -0700526 /** Whether the given RawContactDelta belong to the given account. */
527 private boolean matchesAccount(AccountWithDataSet accountWithDataSet,
528 RawContactDelta rawContactDelta) {
529 if (accountWithDataSet == null) return false;
530 return Objects.equals(accountWithDataSet.name, rawContactDelta.getAccountName())
531 && Objects.equals(accountWithDataSet.type, rawContactDelta.getAccountType())
532 && Objects.equals(accountWithDataSet.dataSet, rawContactDelta.getDataSet());
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700533 }
534
Walter Jang708ea9e2015-09-10 15:42:05 -0700535 private void addAccountInfo() {
536 if (mPrimaryRawContactDelta == null) {
Walter Jang708ea9e2015-09-10 15:42:05 -0700537 mAccountHeaderContainer.setVisibility(View.GONE);
538 mAccountSelectorContainer.setVisibility(View.GONE);
539 return;
540 }
541
542 // Get the account information for the primary raw contact delta
543 final Pair<String,String> accountInfo = EditorUiUtils.getAccountInfo(getContext(),
544 mIsUserProfile, mPrimaryRawContactDelta.getAccountName(),
545 mPrimaryRawContactDelta.getAccountType(mAccountTypeManager));
546
547 // The account header and selector show the same information so both shouldn't be visible
548 // at the same time
549 final List<AccountWithDataSet> accounts =
550 AccountTypeManager.getInstance(getContext()).getAccounts(true);
551 if (mHasNewContact && !mIsUserProfile && accounts.size() > 1) {
552 mAccountHeaderContainer.setVisibility(View.GONE);
553 addAccountSelector(accountInfo);
554 } else {
555 addAccountHeader(accountInfo);
556 mAccountSelectorContainer.setVisibility(View.GONE);
557 }
558 }
559
560 private void addAccountHeader(Pair<String,String> accountInfo) {
Walter Jang03cea2e2015-09-18 17:04:21 -0700561 if (TextUtils.isEmpty(accountInfo.first)) {
Walter Jang708ea9e2015-09-10 15:42:05 -0700562 // Hide this view so the other text view will be centered vertically
563 mAccountHeaderName.setVisibility(View.GONE);
564 } else {
565 mAccountHeaderName.setVisibility(View.VISIBLE);
566 mAccountHeaderName.setText(accountInfo.first);
567 }
568 mAccountHeaderType.setText(accountInfo.second);
569
570 mAccountHeaderContainer.setContentDescription(
571 EditorUiUtils.getAccountInfoContentDescription(
572 accountInfo.first, accountInfo.second));
573 }
574
575 private void addAccountSelector(Pair<String,String> accountInfo) {
576 mAccountSelectorContainer.setVisibility(View.VISIBLE);
577
Walter Jang03cea2e2015-09-18 17:04:21 -0700578 if (TextUtils.isEmpty(accountInfo.first)) {
Walter Jang708ea9e2015-09-10 15:42:05 -0700579 // Hide this view so the other text view will be centered vertically
580 mAccountSelectorName.setVisibility(View.GONE);
581 } else {
582 mAccountSelectorName.setVisibility(View.VISIBLE);
583 mAccountSelectorName.setText(accountInfo.first);
584 }
585 mAccountSelectorType.setText(accountInfo.second);
586
587 mAccountSelectorContainer.setContentDescription(
588 EditorUiUtils.getAccountInfoContentDescription(
589 accountInfo.first, accountInfo.second));
590
591 mAccountSelector.setOnClickListener(new View.OnClickListener() {
592 @Override
593 public void onClick(View v) {
594 final ListPopupWindow popup = new ListPopupWindow(getContext(), null);
595 final AccountsListAdapter adapter =
596 new AccountsListAdapter(getContext(),
597 AccountsListAdapter.AccountListFilter.ACCOUNTS_CONTACT_WRITABLE,
598 mPrimaryAccount);
599 popup.setWidth(mAccountSelectorContainer.getWidth());
600 popup.setAnchorView(mAccountSelectorContainer);
601 popup.setAdapter(adapter);
602 popup.setModal(true);
603 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
604 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
605 @Override
606 public void onItemClick(AdapterView<?> parent, View view, int position,
607 long id) {
608 UiClosables.closeQuietly(popup);
609 final AccountWithDataSet newAccount = adapter.getItem(position);
610 if (mListener != null && !mPrimaryAccount.equals(newAccount)) {
611 mListener.onRebindEditorsForNewContact(
612 mPrimaryRawContactDelta,
613 mPrimaryAccount,
614 newAccount);
615 }
616 }
617 });
618 popup.show();
619 }
620 });
621 }
622
Walter Jang363d3fd2015-09-16 10:29:07 -0700623 private void addPhotoView() {
624 // Get the kind section data and values delta that will back the photo view
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700625 Pair<KindSectionData,ValuesDelta> pair = getPrimaryKindSectionData(mPhotoId);
Walter Jang363d3fd2015-09-16 10:29:07 -0700626 if (pair == null) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700627 wlog("photo: no kind section data parsed");
Walter Jang363d3fd2015-09-16 10:29:07 -0700628 return;
629 }
630 final KindSectionData kindSectionData = pair.first;
631 final ValuesDelta valuesDelta = pair.second;
Walter Jang06f73a12015-06-17 11:15:48 -0700632
Walter Jang363d3fd2015-09-16 10:29:07 -0700633 // If we're editing a read-only contact we want to display the photo from the
634 // read-only contact in a photo editor backed by the new raw contact
Walter Jang192a01c2015-09-22 15:23:55 -0700635 // that was created.
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700636 if (mHasNewContact) {
637 mPhotoRawContactId = mPrimaryRawContactDelta == null
638 ? null : mPrimaryRawContactDelta.getRawContactId();
Walter Jang363d3fd2015-09-16 10:29:07 -0700639 }
Walter Jang06f73a12015-06-17 11:15:48 -0700640
Walter Jang363d3fd2015-09-16 10:29:07 -0700641 mPhotoRawContactId = kindSectionData.getRawContactDelta().getRawContactId();
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700642 mPhotoView.setValues(kindSectionData.getDataKind(), valuesDelta,
Walter Jang363d3fd2015-09-16 10:29:07 -0700643 kindSectionData.getRawContactDelta(),
644 !kindSectionData.getAccountType().areContactsWritable(), mMaterialPalette,
645 mViewIdGenerator);
646 }
647
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700648 private Pair<KindSectionData,ValuesDelta> getPrimaryKindSectionData(long id) {
649 final String mimeType = Photo.CONTENT_ITEM_TYPE;
Walter Jang363d3fd2015-09-16 10:29:07 -0700650 final List<KindSectionData> kindSectionDataList = mKindSectionDataMap.get(mimeType);
651 if (kindSectionDataList == null || kindSectionDataList.isEmpty()) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700652 wlog("photo: no kind section data parsed");
Walter Jang363d3fd2015-09-16 10:29:07 -0700653 return null;
654 }
Walter Jang151f3e62015-02-26 15:29:40 -0800655
Walter Jang363d3fd2015-09-16 10:29:07 -0700656 KindSectionData resultKindSectionData = null;
657 ValuesDelta resultValuesDelta = null;
658 if (id > 0) {
659 // Look for a match for the ID that was passed in
660 for (KindSectionData kindSectionData : kindSectionDataList) {
661 resultValuesDelta = kindSectionData.getValuesDeltaById(id);
662 if (resultValuesDelta != null) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700663 vlog("photo: matched kind section data by ID");
Walter Jang363d3fd2015-09-16 10:29:07 -0700664 resultKindSectionData = kindSectionData;
665 break;
Walter Jang398cd4b2015-06-16 11:17:53 -0700666 }
667 }
668 }
Walter Jang363d3fd2015-09-16 10:29:07 -0700669 if (resultKindSectionData == null) {
670 // Look for a super primary photo
671 for (KindSectionData kindSectionData : kindSectionDataList) {
672 resultValuesDelta = kindSectionData.getSuperPrimaryValuesDelta();
673 if (resultValuesDelta != null) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700674 wlog("photo: matched super primary kind section data");
Walter Jang363d3fd2015-09-16 10:29:07 -0700675 resultKindSectionData = kindSectionData;
676 break;
677 }
Walter Jang151f3e62015-02-26 15:29:40 -0800678 }
679 }
Walter Jang363d3fd2015-09-16 10:29:07 -0700680 if (resultKindSectionData == null) {
681 // Fall back to the first non-empty value
682 for (KindSectionData kindSectionData : kindSectionDataList) {
683 resultValuesDelta = kindSectionData.getFirstNonEmptyValuesDelta();
684 if (resultValuesDelta != null) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700685 vlog("photo: using first non empty value");
Walter Jang363d3fd2015-09-16 10:29:07 -0700686 resultKindSectionData = kindSectionData;
687 break;
688 }
Walter Jang151f3e62015-02-26 15:29:40 -0800689 }
690 }
Walter Jang363d3fd2015-09-16 10:29:07 -0700691 if (resultKindSectionData == null || resultValuesDelta == null) {
692 final List<ValuesDelta> valuesDeltaList = kindSectionDataList.get(0).getValuesDeltas();
693 if (valuesDeltaList != null && !valuesDeltaList.isEmpty()) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700694 vlog("photo: falling back to first empty entry");
Walter Jang363d3fd2015-09-16 10:29:07 -0700695 resultValuesDelta = valuesDeltaList.get(0);
696 resultKindSectionData = kindSectionDataList.get(0);
Walter Jang10446452015-02-20 13:51:16 -0800697 }
698 }
Walter Jang363d3fd2015-09-16 10:29:07 -0700699 return resultKindSectionData != null && resultValuesDelta != null
700 ? new Pair<>(resultKindSectionData, resultValuesDelta) : null;
Walter Jang10446452015-02-20 13:51:16 -0800701 }
702
Walter Jangf5dfea42015-09-16 12:30:36 -0700703 private void addKindSectionViews() {
704 // Sort the kinds
705 final TreeSet<Map.Entry<String,List<KindSectionData>>> entries =
Walter Jang192a01c2015-09-22 15:23:55 -0700706 new TreeSet<>(KIND_SECTION_DATA_MAP_ENTRY_COMPARATOR);
Walter Jangf5dfea42015-09-16 12:30:36 -0700707 entries.addAll(mKindSectionDataMap.entrySet());
Walter Jang3efae4a2015-02-18 11:12:00 -0800708
Walter Jangf5dfea42015-09-16 12:30:36 -0700709 vlog("kind: " + entries.size() + " kindSection(s)");
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700710 int i = -1;
Walter Jangf5dfea42015-09-16 12:30:36 -0700711 for (Map.Entry<String, List<KindSectionData>> entry : entries) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700712 i++;
713
Walter Jangf5dfea42015-09-16 12:30:36 -0700714 final String mimeType = entry.getKey();
715 final List<KindSectionData> kindSectionDataList = entry.getValue();
Walter Jangab50e6f2015-06-15 08:57:22 -0700716
Walter Jangf5dfea42015-09-16 12:30:36 -0700717 // Ignore mime types that we've already handled
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700718 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
719 vlog("kind: " + i + " " + mimeType + " dropped");
720 continue;
721 }
Walter Jangf5dfea42015-09-16 12:30:36 -0700722
723 // Ignore mime types that we don't handle
Walter Jang192a01c2015-09-22 15:23:55 -0700724 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700725 vlog("kind: " + i + " " + mimeType + " dropped");
726 continue;
727 }
Walter Jangf5dfea42015-09-16 12:30:36 -0700728
Walter Jang192a01c2015-09-22 15:23:55 -0700729 if (kindSectionDataList != null && !kindSectionDataList.isEmpty()) {
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700730 vlog("kind: " + i + " " + mimeType + ": " + kindSectionDataList.size() +
731 " kindSectionData(s)");
732
Walter Jangf5dfea42015-09-16 12:30:36 -0700733 final CompactKindSectionView kindSectionView = inflateKindSectionView(
734 mKindSectionViews, kindSectionDataList, mimeType);
Walter Jangf5dfea42015-09-16 12:30:36 -0700735 mKindSectionViews.addView(kindSectionView);
Walter Jangcab3dce2015-02-09 17:48:03 -0800736 }
737 }
738 }
739
Walter Jangf5dfea42015-09-16 12:30:36 -0700740 private CompactKindSectionView inflateKindSectionView(ViewGroup viewGroup,
741 List<KindSectionData> kindSectionDataList, String mimeType) {
742 final CompactKindSectionView kindSectionView = (CompactKindSectionView)
743 mLayoutInflater.inflate(R.layout.compact_item_kind_section, viewGroup,
744 /* attachToRoot =*/ false);
745
Walter Jang192a01c2015-09-22 15:23:55 -0700746 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
Walter Jangf5dfea42015-09-16 12:30:36 -0700747 || Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
Walter Jang192a01c2015-09-22 15:23:55 -0700748 // Phone numbers and email addresses are always displayed,
749 // even if they are empty
Walter Jangf5dfea42015-09-16 12:30:36 -0700750 kindSectionView.setHideWhenEmpty(false);
751 }
Walter Jangf5dfea42015-09-16 12:30:36 -0700752
Walter Jang192a01c2015-09-22 15:23:55 -0700753 // Since phone numbers and email addresses displayed even if they are empty,
754 // they will be the only types you add new values to initially for new contacts
755 kindSectionView.setShowOneEmptyEditor(true);
756
757 // Sort so the editors wind up in the order we want
758 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
759 Collections.sort(kindSectionDataList, new NameEditorComparator(getContext(),
760 mPrimaryRawContactDelta));
761 } else {
762 Collections.sort(kindSectionDataList, new EditorComparator(getContext()));
Walter Jang3e5ae0d2015-09-20 12:43:37 -0700763 }
764
765 kindSectionView.setState(kindSectionDataList, /* readOnly =*/ false, mViewIdGenerator,
766 mListener);
Walter Jangf5dfea42015-09-16 12:30:36 -0700767
768 return kindSectionView;
Walter Jangcab3dce2015-02-09 17:48:03 -0800769 }
770
Walter Jangf5dfea42015-09-16 12:30:36 -0700771 private void updateMoreFieldsButton() {
772 // If any kind section views are hidden then show the link
773 for (int i = 0; i < mKindSectionViews.getChildCount(); i++) {
774 final CompactKindSectionView kindSectionView =
775 (CompactKindSectionView) mKindSectionViews.getChildAt(i);
776 if (kindSectionView.getVisibility() == View.GONE) {
777 // Show the more fields button
778 mMoreFields.setVisibility(View.VISIBLE);
779 return;
780 }
781 }
782 // Hide the more fields button
783 mMoreFields.setVisibility(View.GONE);
Walter Jangcab3dce2015-02-09 17:48:03 -0800784 }
785
Walter Jangbf63a6d2015-05-05 09:14:35 -0700786 private static void vlog(String message) {
787 if (Log.isLoggable(TAG, Log.VERBOSE)) {
788 Log.v(TAG, message);
Walter Jangcab3dce2015-02-09 17:48:03 -0800789 }
790 }
Walter Jang363d3fd2015-09-16 10:29:07 -0700791
792 private static void wlog(String message) {
793 if (Log.isLoggable(TAG, Log.WARN)) {
794 Log.w(TAG, message);
795 }
796 }
797
798 private static void elog(String message) {
799 Log.e(TAG, message);
800 }
Walter Jangcab3dce2015-02-09 17:48:03 -0800801}