blob: bc0c7f885e27d70692ca97d1d979723abcf8f940 [file] [log] [blame]
Walter Jang3f990ba2015-01-27 17:38:30 +00001/*
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
Walter Jang7b0970f2016-09-01 10:40:19 -070019import android.accounts.Account;
Walter Jang49ed2032015-02-11 20:09:05 -080020import android.app.Activity;
Walter Jang7b0970f2016-09-01 10:40:19 -070021import android.app.Fragment;
22import android.app.LoaderManager;
James Laskeye5a140a2016-10-18 15:43:42 -070023import android.content.ContentResolver;
Walter Jang7b0970f2016-09-01 10:40:19 -070024import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.CursorLoader;
Walter Jang3f990ba2015-01-27 17:38:30 +000028import android.content.Intent;
Walter Jang7b0970f2016-09-01 10:40:19 -070029import android.content.Loader;
30import android.database.Cursor;
Walter Jang3efae4a2015-02-18 11:12:00 -080031import android.graphics.Bitmap;
32import android.net.Uri;
Walter Jang3f990ba2015-01-27 17:38:30 +000033import android.os.Bundle;
Walter Jang7b0970f2016-09-01 10:40:19 -070034import android.os.SystemClock;
35import android.provider.ContactsContract;
36import android.provider.ContactsContract.CommonDataKinds.Email;
37import android.provider.ContactsContract.CommonDataKinds.Event;
38import android.provider.ContactsContract.CommonDataKinds.Organization;
39import android.provider.ContactsContract.CommonDataKinds.Phone;
40import android.provider.ContactsContract.CommonDataKinds.StructuredName;
41import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
42import android.provider.ContactsContract.Intents;
43import android.provider.ContactsContract.RawContacts;
Walter Jange3945952015-10-27 12:44:54 -070044import android.text.TextUtils;
Walter Jangcab3dce2015-02-09 17:48:03 -080045import android.util.Log;
Walter Jang3f990ba2015-01-27 17:38:30 +000046import android.view.LayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -070047import android.view.Menu;
48import android.view.MenuInflater;
Walter Jangc90cc152015-06-19 14:15:08 -070049import android.view.MenuItem;
Walter Jang3f990ba2015-01-27 17:38:30 +000050import android.view.View;
51import android.view.ViewGroup;
Walter Jang7b0970f2016-09-01 10:40:19 -070052import android.widget.AdapterView;
53import android.widget.BaseAdapter;
Walter Jang3f990ba2015-01-27 17:38:30 +000054import android.widget.LinearLayout;
Walter Jang7b0970f2016-09-01 10:40:19 -070055import android.widget.ListPopupWindow;
Walter Jang79658e12015-09-24 10:36:26 -070056import android.widget.Toast;
Gary Mai15646ce2016-11-17 10:54:01 -080057import android.widget.Toolbar;
Walter Jang3f990ba2015-01-27 17:38:30 +000058
Walter Jang7b0970f2016-09-01 10:40:19 -070059import com.android.contacts.ContactSaveService;
Gary Mai0a49afa2016-12-05 15:53:58 -080060import com.android.contacts.Experiments;
Walter Jang7b0970f2016-09-01 10:40:19 -070061import com.android.contacts.GroupMetaDataLoader;
62import com.android.contacts.R;
Gary Maia4adae12016-10-23 13:47:17 -070063import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
Gary Mai363af602016-09-28 10:01:23 -070064import com.android.contacts.activities.ContactEditorActivity;
65import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
Walter Jang7b0970f2016-09-01 10:40:19 -070066import com.android.contacts.activities.ContactSelectionActivity;
Gary Mai0a49afa2016-12-05 15:53:58 -080067import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
68import com.android.contacts.group.GroupUtil;
69import com.android.contacts.list.UiIntentActions;
Gary Mai69c182a2016-12-05 13:07:03 -080070import com.android.contacts.logging.ScreenEvent.ScreenType;
71import com.android.contacts.model.AccountTypeManager;
72import com.android.contacts.model.Contact;
73import com.android.contacts.model.ContactLoader;
74import com.android.contacts.model.RawContact;
75import com.android.contacts.model.RawContactDelta;
76import com.android.contacts.model.RawContactDeltaList;
77import com.android.contacts.model.RawContactModifier;
78import com.android.contacts.model.ValuesDelta;
79import com.android.contacts.model.account.AccountType;
80import com.android.contacts.model.account.AccountWithDataSet;
81import com.android.contacts.preference.ContactsPreferences;
Gary Maie4874662016-09-26 11:42:54 -070082import com.android.contacts.quickcontact.InvisibleContactUtil;
Walter Jang7b0970f2016-09-01 10:40:19 -070083import com.android.contacts.quickcontact.QuickContactActivity;
Gary Mai0a49afa2016-12-05 15:53:58 -080084import com.android.contacts.util.ContactDisplayUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070085import com.android.contacts.util.ContactPhotoUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080086import com.android.contacts.util.ImplicitIntentsUtil;
87import com.android.contacts.util.MaterialColorMapUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070088import com.android.contacts.util.UiClosables;
Gary Maia4adae12016-10-23 13:47:17 -070089import com.android.contactsbind.HelpUtils;
Walter Jang581585d2016-09-21 19:21:13 -070090import com.android.contactsbind.ObjectFactory;
91import com.android.contactsbind.experiments.Flags;
Walter Jang7b0970f2016-09-01 10:40:19 -070092
93import com.google.common.collect.ImmutableList;
94import com.google.common.collect.Lists;
95
Walter Jang3efae4a2015-02-18 11:12:00 -080096import java.io.FileNotFoundException;
Walter Jang31a74ad2015-10-02 19:17:39 -070097import java.util.ArrayList;
Walter Jang7b0970f2016-09-01 10:40:19 -070098import java.util.HashSet;
99import java.util.Iterator;
100import java.util.List;
101import java.util.Set;
Walter Jang3efae4a2015-02-18 11:12:00 -0800102
Walter Jang3f990ba2015-01-27 17:38:30 +0000103/**
104 * Contact editor with only the most important fields displayed initially.
105 */
Gary Mai363af602016-09-28 10:01:23 -0700106public class ContactEditorFragment extends Fragment implements
Walter Jang7b0970f2016-09-01 10:40:19 -0700107 ContactEditor, SplitContactConfirmationDialogFragment.Listener,
108 JoinContactConfirmationDialogFragment.Listener,
109 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
110 CancelEditDialogFragment.Listener,
Gary Mai363af602016-09-28 10:01:23 -0700111 RawContactEditorView.Listener, PhotoEditorView.Listener {
Walter Jang3f990ba2015-01-27 17:38:30 +0000112
Walter Jang7b0970f2016-09-01 10:40:19 -0700113 static final String TAG = "ContactEditor";
114
115 private static final int LOADER_CONTACT = 1;
116 private static final int LOADER_GROUPS = 2;
117
Walter Jang3efae4a2015-02-18 11:12:00 -0800118 private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
Walter Jang28a27272015-09-19 16:06:08 -0700119 private static final String KEY_UPDATED_PHOTOS = "updated_photos";
Walter Jang3efae4a2015-02-18 11:12:00 -0800120
Walter Jang7b0970f2016-09-01 10:40:19 -0700121 private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
122 add(Intent.ACTION_EDIT);
123 add(Intent.ACTION_INSERT);
Gary Mai363af602016-09-28 10:01:23 -0700124 add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
Walter Jang7b0970f2016-09-01 10:40:19 -0700125 }};
126
127 private static final String KEY_ACTION = "action";
128 private static final String KEY_URI = "uri";
129 private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
130 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
131 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
132 private static final String KEY_MATERIAL_PALETTE = "materialPalette";
Gary Maic135a5d2016-12-19 11:13:46 -0800133 private static final String KEY_ACCOUNT = "saveToAccount";
Walter Jang7b0970f2016-09-01 10:40:19 -0700134 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
135
136 private static final String KEY_RAW_CONTACTS = "rawContacts";
137
138 private static final String KEY_EDIT_STATE = "state";
139 private static final String KEY_STATUS = "status";
140
141 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
142 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
143
144 private static final String KEY_IS_EDIT = "isEdit";
145 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
146
Walter Jang7b0970f2016-09-01 10:40:19 -0700147 private static final String KEY_IS_USER_PROFILE = "isUserProfile";
148
149 private static final String KEY_ENABLED = "enabled";
150
151 // Aggregation PopupWindow
152 private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
153 "aggregationSuggestionsRawContactId";
154
155 // Join Activity
156 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
157
Gary Mai698cee72016-09-19 16:09:54 -0700158 private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
159 private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
Walter Jang7b0970f2016-09-01 10:40:19 -0700160
161 protected static final int REQUEST_CODE_JOIN = 0;
162 protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700163
Walter Jang7b0970f2016-09-01 10:40:19 -0700164 /**
165 * An intent extra that forces the editor to add the edited contact
166 * to the default group (e.g. "My Contacts").
167 */
168 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
169
170 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
171
172 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
173 "disableDeleteMenuOption";
174
175 /**
176 * Intent key to pass the photo palette primary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700177 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700178 */
179 public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
180 "material_palette_primary_color";
181
182 /**
183 * Intent key to pass the photo palette secondary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700184 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700185 */
186 public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
187 "material_palette_secondary_color";
188
189 /**
190 * Intent key to pass the ID of the photo to display on the editor.
191 */
Gary Maida20b472016-09-20 14:46:40 -0700192 // TODO: This can be cleaned up if we decide to not pass the photo id through
193 // QuickContactActivity.
Walter Jang7b0970f2016-09-01 10:40:19 -0700194 public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
195
196 /**
Gary Maia6c80b32016-09-30 16:34:55 -0700197 * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
198 * by itself.
199 */
200 public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
201 "raw_contact_id_to_display_alone";
202
203 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700204 * Intent extra to specify a {@link ContactEditor.SaveMode}.
205 */
206 public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
207
208 /**
209 * Intent extra key for the contact ID to join the current contact to after saving.
210 */
211 public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
212
213 /**
214 * Callbacks for Activities that host contact editors Fragments.
215 */
216 public interface Listener {
217
218 /**
219 * Contact was not found, so somehow close this fragment. This is raised after a contact
220 * is removed via Menu/Delete
221 */
222 void onContactNotFound();
223
224 /**
225 * Contact was split, so we can close now.
226 *
227 * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
228 * The editor tries best to chose the most natural contact here.
229 */
230 void onContactSplit(Uri newLookupUri);
231
232 /**
233 * User has tapped Revert, close the fragment now.
234 */
235 void onReverted();
236
237 /**
238 * Contact was saved and the Fragment can now be closed safely.
239 */
240 void onSaveFinished(Intent resultIntent);
241
242 /**
Gary Mai678108e2016-10-26 14:34:33 -0700243 * User switched to editing a different raw contact (a suggestion from the
Walter Jang7b0970f2016-09-01 10:40:19 -0700244 * aggregation engine).
245 */
Gary Mai678108e2016-10-26 14:34:33 -0700246 void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
Walter Jang7b0970f2016-09-01 10:40:19 -0700247 ArrayList<ContentValues> contentValues);
248
249 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700250 * User has requested that contact be deleted.
251 */
252 void onDeleteRequested(Uri contactUri);
253 }
254
255 /**
256 * Adapter for aggregation suggestions displayed in a PopupWindow when
257 * editor fields change.
258 */
259 private static final class AggregationSuggestionAdapter extends BaseAdapter {
260 private final LayoutInflater mLayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -0700261 private final AggregationSuggestionView.Listener mListener;
262 private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
263
Gary Mai678108e2016-10-26 14:34:33 -0700264 public AggregationSuggestionAdapter(Activity activity,
Walter Jang7b0970f2016-09-01 10:40:19 -0700265 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
266 mLayoutInflater = activity.getLayoutInflater();
Walter Jang7b0970f2016-09-01 10:40:19 -0700267 mListener = listener;
268 mSuggestions = suggestions;
269 }
270
271 @Override
272 public View getView(int position, View convertView, ViewGroup parent) {
273 final Suggestion suggestion = (Suggestion) getItem(position);
274 final AggregationSuggestionView suggestionView =
275 (AggregationSuggestionView) mLayoutInflater.inflate(
276 R.layout.aggregation_suggestions_item, null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700277 suggestionView.setListener(mListener);
278 suggestionView.bindSuggestion(suggestion);
279 return suggestionView;
280 }
281
282 @Override
283 public long getItemId(int position) {
284 return position;
285 }
286
287 @Override
288 public Object getItem(int position) {
289 return mSuggestions.get(position);
290 }
291
292 @Override
293 public int getCount() {
294 return mSuggestions.size();
295 }
296 }
297
298 protected Context mContext;
299 protected Listener mListener;
300
301 //
302 // Views
303 //
304 protected LinearLayout mContent;
Walter Jang7b0970f2016-09-01 10:40:19 -0700305 protected ListPopupWindow mAggregationSuggestionPopup;
306
307 //
308 // Parameters passed in on {@link #load}
309 //
310 protected String mAction;
311 protected Uri mLookupUri;
312 protected Bundle mIntentExtras;
313 protected boolean mAutoAddToDefaultGroup;
314 protected boolean mDisableDeleteMenuOption;
315 protected boolean mNewLocalProfile;
316 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang7b0970f2016-09-01 10:40:19 -0700317
318 //
319 // Helpers
320 //
321 protected ContactEditorUtils mEditorUtils;
322 protected RawContactDeltaComparator mComparator;
323 protected ViewIdGenerator mViewIdGenerator;
324 private AggregationSuggestionEngine mAggregationSuggestionEngine;
325
326 //
327 // Loaded data
328 //
329 // Used to store existing contact data so it can be re-applied during a rebind call,
330 // i.e. account switch.
Gary Mai7b751452016-11-07 17:04:04 -0800331 protected Contact mContact;
Walter Jang7b0970f2016-09-01 10:40:19 -0700332 protected ImmutableList<RawContact> mRawContacts;
333 protected Cursor mGroupMetaData;
334
335 //
336 // Editor state
337 //
338 protected RawContactDeltaList mState;
339 protected int mStatus;
340 protected long mRawContactIdToDisplayAlone = -1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700341
342 // Whether to show the new contact blank form and if it's corresponding delta is ready.
343 protected boolean mHasNewContact;
344 protected AccountWithDataSet mAccountWithDataSet;
345 protected boolean mNewContactDataReady;
346 protected boolean mNewContactAccountChanged;
347
348 // Whether it's an edit of existing contact and if it's corresponding delta is ready.
349 protected boolean mIsEdit;
350 protected boolean mExistingContactDataReady;
351
352 // Whether we are editing the "me" profile
353 protected boolean mIsUserProfile;
354
Walter Jang7b0970f2016-09-01 10:40:19 -0700355 // Whether editor views and options menu items should be enabled
356 private boolean mEnabled = true;
357
358 // Aggregation PopupWindow
359 private long mAggregationSuggestionsRawContactId;
360
361 // Join Activity
362 protected long mContactIdForJoin;
363
364 // Used to pre-populate the editor with a display name when a user edits a read-only contact.
Gary Mai698cee72016-09-19 16:09:54 -0700365 protected long mReadOnlyDisplayNameId;
366 protected boolean mCopyReadOnlyName;
Walter Jang7b0970f2016-09-01 10:40:19 -0700367
368 /**
369 * The contact data loader listener.
370 */
371 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
372 new LoaderManager.LoaderCallbacks<Contact>() {
373
374 protected long mLoaderStartTime;
375
376 @Override
377 public Loader<Contact> onCreateLoader(int id, Bundle args) {
378 mLoaderStartTime = SystemClock.elapsedRealtime();
Gary Maie4874662016-09-26 11:42:54 -0700379 return new ContactLoader(mContext, mLookupUri,
380 /* postViewNotification */ true,
381 /* loadGroupMetaData */ true);
Walter Jang7b0970f2016-09-01 10:40:19 -0700382 }
383
384 @Override
385 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
386 final long loaderCurrentTime = SystemClock.elapsedRealtime();
387 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
388 if (!contact.isLoaded()) {
389 // Item has been deleted. Close activity without saving again.
390 Log.i(TAG, "No contact found. Closing activity");
391 mStatus = Status.CLOSING;
392 if (mListener != null) mListener.onContactNotFound();
393 return;
394 }
395
396 mStatus = Status.EDITING;
397 mLookupUri = contact.getLookupUri();
398 final long setDataStartTime = SystemClock.elapsedRealtime();
399 setState(contact);
Walter Jang7b0970f2016-09-01 10:40:19 -0700400 final long setDataEndTime = SystemClock.elapsedRealtime();
401
402 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
403 }
404
405 @Override
406 public void onLoaderReset(Loader<Contact> loader) {
407 }
408 };
409
410 /**
411 * The groups meta data loader listener.
412 */
413 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
414 new LoaderManager.LoaderCallbacks<Cursor>() {
415
416 @Override
417 public CursorLoader onCreateLoader(int id, Bundle args) {
Gary Mai5c1bff22016-09-30 15:10:25 -0700418 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
419 GroupUtil.ALL_GROUPS_SELECTION);
Walter Jang7b0970f2016-09-01 10:40:19 -0700420 }
421
422 @Override
423 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
424 mGroupMetaData = data;
425 setGroupMetaData();
426 }
427
428 @Override
429 public void onLoaderReset(Loader<Cursor> loader) {
430 }
431 };
432
Walter Jang3efae4a2015-02-18 11:12:00 -0800433 private long mPhotoRawContactId;
Walter Jang28a27272015-09-19 16:06:08 -0700434 private Bundle mUpdatedPhotos = new Bundle();
Walter Jang3efae4a2015-02-18 11:12:00 -0800435
436 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700437 public Context getContext() {
438 return getActivity();
439 }
440
441 @Override
442 public void onAttach(Activity activity) {
443 super.onAttach(activity);
444 mContext = activity;
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700445 mEditorUtils = ContactEditorUtils.create(mContext);
Walter Jang7b0970f2016-09-01 10:40:19 -0700446 mComparator = new RawContactDeltaComparator(mContext);
447 }
448
449 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800450 public void onCreate(Bundle savedState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700451 if (savedState != null) {
452 // Restore mUri before calling super.onCreate so that onInitializeLoaders
453 // would already have a uri and an action to work with
454 mAction = savedState.getString(KEY_ACTION);
455 mLookupUri = savedState.getParcelable(KEY_URI);
456 }
457
Walter Jang3efae4a2015-02-18 11:12:00 -0800458 super.onCreate(savedState);
459
Walter Jang7b0970f2016-09-01 10:40:19 -0700460 if (savedState == null) {
461 mViewIdGenerator = new ViewIdGenerator();
462
463 // mState can still be null because it may not have have finished loading before
464 // onSaveInstanceState was called.
465 mState = new RawContactDeltaList();
466 } else {
467 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
468
469 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
470 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
471 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
472 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
Gary Maic135a5d2016-12-19 11:13:46 -0800473 mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
Walter Jang7b0970f2016-09-01 10:40:19 -0700474 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
475 KEY_RAW_CONTACTS));
476 // NOTE: mGroupMetaData is not saved/restored
477
478 // Read state from savedState. No loading involved here
479 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
480 mStatus = savedState.getInt(KEY_STATUS);
Walter Jang7b0970f2016-09-01 10:40:19 -0700481
482 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
483 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
484
485 mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
486 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
487
488 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
489
Walter Jang7b0970f2016-09-01 10:40:19 -0700490 mEnabled = savedState.getBoolean(KEY_ENABLED);
491
492 // Aggregation PopupWindow
493 mAggregationSuggestionsRawContactId = savedState.getLong(
494 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
495
496 // Join Activity
497 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
498
Gary Mai698cee72016-09-19 16:09:54 -0700499 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
500 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700501
Walter Jang3efae4a2015-02-18 11:12:00 -0800502 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
Walter Jang28a27272015-09-19 16:06:08 -0700503 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
Walter Jang3efae4a2015-02-18 11:12:00 -0800504 }
505 }
506
Walter Jang3f990ba2015-01-27 17:38:30 +0000507 @Override
Walter Jang3f990ba2015-01-27 17:38:30 +0000508 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Walter Janged8f6c92015-01-30 16:07:47 -0800509 setHasOptionsMenu(true);
510
Walter Jang3f990ba2015-01-27 17:38:30 +0000511 final View view = inflater.inflate(
Gary Mai363af602016-09-28 10:01:23 -0700512 R.layout.contact_editor_fragment, container, false);
Walter Jangf5dfea42015-09-16 12:30:36 -0700513 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
Walter Jang3f990ba2015-01-27 17:38:30 +0000514 return view;
515 }
516
Walter Janged8f6c92015-01-30 16:07:47 -0800517 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700518 public void onActivityCreated(Bundle savedInstanceState) {
519 super.onActivityCreated(savedInstanceState);
520
521 validateAction(mAction);
522
523 if (mState.isEmpty()) {
524 // The delta list may not have finished loading before orientation change happens.
525 // In this case, there will be a saved state but deltas will be missing. Reload from
526 // database.
527 if (Intent.ACTION_EDIT.equals(mAction)) {
528 // Either
529 // 1) orientation change but load never finished.
530 // 2) not an orientation change so data needs to be loaded for first time.
531 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
532 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
533 }
534 } else {
535 // Orientation change, we already have mState, it was loaded by onCreate
536 bindEditors();
537 }
538
539 // Handle initial actions only when existing state missing
540 if (savedInstanceState == null) {
541 final Account account = mIntentExtras == null ? null :
542 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
543 final String dataSet = mIntentExtras == null ? null :
544 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
545 if (account != null) {
546 mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
547 }
548
549 if (Intent.ACTION_EDIT.equals(mAction)) {
550 mIsEdit = true;
551 } else if (Intent.ACTION_INSERT.equals(mAction)) {
552 mHasNewContact = true;
553 if (mAccountWithDataSet != null) {
554 createContact(mAccountWithDataSet);
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700555 } else if (mIntentExtras != null && mIntentExtras.getBoolean(
Gary Mai363af602016-09-28 10:01:23 -0700556 ContactEditorActivity.EXTRA_SAVE_TO_DEVICE_FLAG, false)) {
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700557 createContact(null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700558 } else {
559 // No Account specified. Let the user choose
560 // Load Accounts async so that we can present them
561 selectAccountAndCreateContact();
562 }
563 }
564 }
565 }
566
567 /**
568 * Checks if the requested action is valid.
569 *
570 * @param action The action to test.
571 * @throws IllegalArgumentException when the action is invalid.
572 */
573 private static void validateAction(String action) {
574 if (VALID_INTENT_ACTIONS.contains(action)) {
575 return;
576 }
577 throw new IllegalArgumentException(
578 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
579 }
580
581 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800582 public void onSaveInstanceState(Bundle outState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700583 outState.putString(KEY_ACTION, mAction);
584 outState.putParcelable(KEY_URI, mLookupUri);
585 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
586 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
587 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
588 if (mMaterialPalette != null) {
589 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
590 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700591 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
592
593 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
594 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
595 // NOTE: mGroupMetaData is not saved
596
Gary Mai36ceb422016-10-17 14:04:17 -0700597 outState.putParcelable(KEY_EDIT_STATE, mState);
Walter Jang7b0970f2016-09-01 10:40:19 -0700598 outState.putInt(KEY_STATUS, mStatus);
599 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
600 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
601 outState.putBoolean(KEY_IS_EDIT, mIsEdit);
602 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
Gary Maic135a5d2016-12-19 11:13:46 -0800603 outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
Walter Jang7b0970f2016-09-01 10:40:19 -0700604 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
605
Walter Jang7b0970f2016-09-01 10:40:19 -0700606 outState.putBoolean(KEY_ENABLED, mEnabled);
607
608 // Aggregation PopupWindow
609 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
610 mAggregationSuggestionsRawContactId);
611
612 // Join Activity
613 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
614
Gary Mai698cee72016-09-19 16:09:54 -0700615 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
616 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
Walter Jang7b0970f2016-09-01 10:40:19 -0700617
Walter Jang3efae4a2015-02-18 11:12:00 -0800618 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
Walter Jang28a27272015-09-19 16:06:08 -0700619 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
Walter Jang3efae4a2015-02-18 11:12:00 -0800620 super.onSaveInstanceState(outState);
621 }
622
623 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700624 public void onStop() {
625 super.onStop();
626 UiClosables.closeQuietly(mAggregationSuggestionPopup);
627 }
628
629 @Override
630 public void onDestroy() {
631 super.onDestroy();
632 if (mAggregationSuggestionEngine != null) {
633 mAggregationSuggestionEngine.quit();
634 }
635 }
636
637 @Override
638 public void onActivityResult(int requestCode, int resultCode, Intent data) {
639 switch (requestCode) {
640 case REQUEST_CODE_JOIN: {
641 // Ignore failed requests
642 if (resultCode != Activity.RESULT_OK) return;
643 if (data != null) {
644 final long contactId = ContentUris.parseId(data.getData());
645 if (hasPendingChanges()) {
646 // Ask the user if they want to save changes before doing the join
647 JoinContactConfirmationDialogFragment.show(this, contactId);
648 } else {
649 // Do the join immediately
650 joinAggregate(contactId);
651 }
652 }
653 break;
654 }
655 case REQUEST_CODE_ACCOUNTS_CHANGED: {
656 // Bail if the account selector was not successful.
657 if (resultCode != Activity.RESULT_OK) {
658 if (mListener != null) {
659 mListener.onReverted();
660 }
661 return;
662 }
663 // If there's an account specified, use it.
664 if (data != null) {
665 AccountWithDataSet account = data.getParcelableExtra(
666 Intents.Insert.EXTRA_ACCOUNT);
667 if (account != null) {
668 createContact(account);
669 return;
670 }
671 }
672 // If there isn't an account specified, then this is likely a phone-local
673 // contact, so we should continue setting up the editor by automatically selecting
674 // the most appropriate account.
675 createContact();
676 break;
677 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700678 }
679 }
680
Walter Jang7b0970f2016-09-01 10:40:19 -0700681 //
682 // Options menu
683 //
684
Walter Jang7b0970f2016-09-01 10:40:19 -0700685 @Override
686 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
687 inflater.inflate(R.menu.edit_contact, menu);
688 }
689
690 @Override
691 public void onPrepareOptionsMenu(Menu menu) {
692 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
693 // because the custom action bar contains the "save" button now (not the overflow menu).
694 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
695 final MenuItem saveMenu = menu.findItem(R.id.menu_save);
696 final MenuItem splitMenu = menu.findItem(R.id.menu_split);
697 final MenuItem joinMenu = menu.findItem(R.id.menu_join);
Walter Jang7b0970f2016-09-01 10:40:19 -0700698 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
699
Gary Mai5eda2572016-10-11 18:01:32 -0700700 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
701 // on a raw contact level.
702 joinMenu.setVisible(false);
703 splitMenu.setVisible(false);
704 deleteMenu.setVisible(false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700705 // Save menu is invisible when there's only one read only contact in the editor.
Gary Maid7faa652016-10-03 11:53:39 -0700706 saveMenu.setVisible(!isEditingReadOnlyRawContact());
Walter Jang7b0970f2016-09-01 10:40:19 -0700707 if (saveMenu.isVisible()) {
708 // Since we're using a custom action layout we have to manually hook up the handler.
709 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
710 @Override
711 public void onClick(View v) {
712 onOptionsItemSelected(saveMenu);
713 }
714 });
715 }
716
Walter Jang7b0970f2016-09-01 10:40:19 -0700717 int size = menu.size();
718 for (int i = 0; i < size; i++) {
719 menu.getItem(i).setEnabled(mEnabled);
720 }
721 }
722
723 @Override
Walter Jangc90cc152015-06-19 14:15:08 -0700724 public boolean onOptionsItemSelected(MenuItem item) {
725 if (item.getItemId() == android.R.id.home) {
726 return revert();
727 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700728
729 final Activity activity = getActivity();
730 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
731 // If we no longer are attached to a running activity want to
732 // drain this event.
733 return true;
734 }
735
736 switch (item.getItemId()) {
737 case R.id.menu_save:
738 return save(SaveMode.CLOSE);
739 case R.id.menu_delete:
740 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
741 return true;
742 case R.id.menu_split:
743 return doSplitContactAction();
744 case R.id.menu_join:
745 return doJoinContactAction();
Gary Maia4adae12016-10-23 13:47:17 -0700746 case R.id.menu_help:
747 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
Walter Jang7b0970f2016-09-01 10:40:19 -0700748 return true;
749 }
750
751 return false;
Walter Jangc90cc152015-06-19 14:15:08 -0700752 }
753
754 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700755 public boolean revert() {
756 if (mState.isEmpty() || !hasPendingChanges()) {
757 onCancelEditConfirmed();
758 } else {
759 CancelEditDialogFragment.show(this);
760 }
761 return true;
762 }
763
764 @Override
765 public void onCancelEditConfirmed() {
766 // When this Fragment is closed we don't want it to auto-save
767 mStatus = Status.CLOSING;
768 if (mListener != null) {
769 mListener.onReverted();
770 }
771 }
772
773 @Override
774 public void onSplitContactConfirmed(boolean hasPendingChanges) {
775 if (mState.isEmpty()) {
776 // This may happen when this Fragment is recreated by the system during users
777 // confirming the split action (and thus this method is called just before onCreate()),
778 // for example.
779 Log.e(TAG, "mState became null during the user's confirming split action. " +
780 "Cannot perform the save action.");
781 return;
782 }
783
784 if (!hasPendingChanges && mHasNewContact) {
785 // If the user didn't add anything new, we don't want to split out the newly created
786 // raw contact into a name-only contact so remove them.
787 final Iterator<RawContactDelta> iterator = mState.iterator();
788 while (iterator.hasNext()) {
789 final RawContactDelta rawContactDelta = iterator.next();
790 if (rawContactDelta.getRawContactId() < 0) {
791 iterator.remove();
792 }
793 }
794 }
795 mState.markRawContactsForSplitting();
796 save(SaveMode.SPLIT);
797 }
798
Gary Maib9065dd2016-11-08 10:49:00 -0800799 @Override
800 public void onSplitContactCanceled() {}
801
Walter Jang7b0970f2016-09-01 10:40:19 -0700802 private boolean doSplitContactAction() {
803 if (!hasValidState()) return false;
804
805 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
806 return true;
807 }
808
809 private boolean doJoinContactAction() {
810 if (!hasValidState() || mLookupUri == null) {
811 return false;
812 }
813
814 // If we just started creating a new contact and haven't added any data, it's too
815 // early to do a join
816 if (mState.size() == 1 && mState.get(0).isContactInsert()
817 && !hasPendingChanges()) {
818 Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
819 Toast.LENGTH_LONG).show();
820 return true;
821 }
822
823 showJoinAggregateActivity(mLookupUri);
824 return true;
825 }
826
827 @Override
828 public void onJoinContactConfirmed(long joinContactId) {
829 doSaveAction(SaveMode.JOIN, joinContactId);
830 }
831
Walter Jang7b0970f2016-09-01 10:40:19 -0700832 @Override
833 public boolean save(int saveMode) {
834 if (!hasValidState() || mStatus != Status.EDITING) {
835 return false;
836 }
837
838 // If we are about to close the editor - there is no need to refresh the data
Gary Mai363af602016-09-28 10:01:23 -0700839 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
Walter Jang7b0970f2016-09-01 10:40:19 -0700840 || saveMode == SaveMode.SPLIT) {
841 getLoaderManager().destroyLoader(LOADER_CONTACT);
842 }
843
844 mStatus = Status.SAVING;
845
846 if (!hasPendingChanges()) {
847 if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
848 // We don't have anything to save and there isn't even an existing contact yet.
849 // Nothing to do, simply go back to editing mode
850 mStatus = Status.EDITING;
851 return true;
852 }
853 onSaveCompleted(/* hadChanges =*/ false, saveMode,
854 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
855 return true;
856 }
857
858 setEnabled(false);
859
860 return doSaveAction(saveMode, /* joinContactId */ null);
861 }
862
863 //
864 // State accessor methods
865 //
866
867 /**
868 * Check if our internal {@link #mState} is valid, usually checked before
869 * performing user actions.
870 */
871 private boolean hasValidState() {
872 return mState.size() > 0;
873 }
874
875 private boolean isEditingUserProfile() {
876 return mNewLocalProfile || mIsUserProfile;
877 }
878
879 /**
Gary Mai5a00de32016-10-19 18:20:41 -0700880 * Whether the contact being edited is composed of read-only raw contacts
Walter Jang7b0970f2016-09-01 10:40:19 -0700881 * aggregated with a newly created writable raw contact.
882 */
883 private boolean isEditingReadOnlyRawContactWithNewContact() {
Gary Mai5a00de32016-10-19 18:20:41 -0700884 return mHasNewContact && mState.size() > 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700885 }
886
887 /**
Gary Maid7faa652016-10-03 11:53:39 -0700888 * @return true if the single raw contact we're looking at is read-only.
889 */
890 private boolean isEditingReadOnlyRawContact() {
891 return hasValidState() && mRawContactIdToDisplayAlone > 0
892 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
893 .getAccountType(AccountTypeManager.getInstance(mContext))
894 .areContactsWritable();
895 }
896
897 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700898 * Return true if there are any edits to the current contact which need to
899 * be saved.
900 */
901 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
902 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
903 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
904 }
905
906 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700907 * Determines if changes were made in the editor that need to be saved, while taking into
908 * account that name changes are not real for read-only contacts.
909 * See go/editing-read-only-contacts
910 */
911 private boolean hasPendingChanges() {
Gary Mai698cee72016-09-19 16:09:54 -0700912 if (isEditingReadOnlyRawContactWithNewContact()) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700913 // We created a new raw contact delta with a default display name.
914 // We must test for pending changes while ignoring the default display name.
Gary Mai698cee72016-09-19 16:09:54 -0700915 final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId)
916 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
917 final ValuesDelta pendingDelta = mState
918 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
919 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700920 final Set<String> excludedMimeTypes = new HashSet<>();
921 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
922 return hasPendingRawContactChanges(excludedMimeTypes);
923 }
924 return true;
925 }
926 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
927 }
928
929 /**
Gary Mai698cee72016-09-19 16:09:54 -0700930 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
931 * of a read only delta and now we want to check if the copied delta has changes.
932 *
933 * @param before original {@link ValuesDelta}
934 * @param after copied {@link ValuesDelta}
935 * @return true if the copied {@link ValuesDelta} has all the same values in the structured
936 * name fields as the original.
937 */
938 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
Gary Mai5a00de32016-10-19 18:20:41 -0700939 if (before == after) return true;
Gary Mai698cee72016-09-19 16:09:54 -0700940 if (before == null || after == null) return false;
941 final ContentValues original = before.getBefore();
942 final ContentValues pending = after.getAfter();
943 if (original != null && pending != null) {
Gary Maia4adae12016-10-23 13:47:17 -0700944 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
Gary Mai698cee72016-09-19 16:09:54 -0700945 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
946 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
947
948 final String beforePrefix = original.getAsString(StructuredName.PREFIX);
949 final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
950 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
951
952 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
953 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
954 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
955
956 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
957 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
958 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
959
960 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
961 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
962 if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
963
964 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
965 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
966 return TextUtils.equals(beforeSuffix, afterSuffix);
967 }
968 return false;
969 }
970
Walter Jang7b0970f2016-09-01 10:40:19 -0700971 //
972 // Account creation
973 //
974
975 private void selectAccountAndCreateContact() {
976 // If this is a local profile, then skip the logic about showing the accounts changed
977 // activity and create a phone-local contact.
978 if (mNewLocalProfile) {
979 createContact(null);
980 return;
981 }
982
983 // If there is no default account or the accounts have changed such that we need to
984 // prompt the user again, then launch the account prompt.
985 if (mEditorUtils.shouldShowAccountChangedNotification()) {
986 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
987 // Prevent a second instance from being started on rotates
988 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
989 mStatus = Status.SUB_ACTIVITY;
990 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
991 } else {
Gary Mai3107b252016-11-02 18:26:07 -0700992 // Make sure the default account is automatically set if there is only one non-device
993 // account.
994 mEditorUtils.maybeUpdateDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -0700995 // Otherwise, there should be a default account. Then either create a local contact
996 // (if default account is null) or create a contact with the specified account.
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700997 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -0700998 createContact(defaultAccount);
999 }
1000 }
1001
1002 /**
1003 * Create a contact by automatically selecting the first account. If there's no available
1004 * account, a device-local contact should be created.
1005 */
1006 private void createContact() {
1007 final List<AccountWithDataSet> accounts =
1008 AccountTypeManager.getInstance(mContext).getAccounts(true);
1009 // No Accounts available. Create a phone-local contact.
1010 if (accounts.isEmpty()) {
1011 createContact(null);
1012 return;
1013 }
1014
1015 // We have an account switcher in "create-account" screen, so don't need to ask a user to
1016 // select an account here.
1017 createContact(accounts.get(0));
1018 }
1019
1020 /**
1021 * Shows account creation screen associated with a given account.
1022 *
1023 * @param account may be null to signal a device-local contact should be created.
1024 */
1025 private void createContact(AccountWithDataSet account) {
1026 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1027 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1028
Gary Maiaebf3202016-09-22 18:11:15 -07001029 setStateForNewContact(account, accountType, isEditingUserProfile());
Walter Jang7b0970f2016-09-01 10:40:19 -07001030 }
1031
1032 //
1033 // Data binding
1034 //
1035
1036 private void setState(Contact contact) {
1037 // If we have already loaded data, we do not want to change it here to not confuse the user
1038 if (!mState.isEmpty()) {
1039 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1040 return;
1041 }
Gary Mai7b751452016-11-07 17:04:04 -08001042 mContact = contact;
Gary Mai4ceabed2016-09-16 12:14:13 -07001043 mRawContacts = contact.getRawContacts();
Walter Jang7b0970f2016-09-01 10:40:19 -07001044
Walter Jang7b0970f2016-09-01 10:40:19 -07001045 // Check for writable raw contacts. If there are none, then we need to create one so user
1046 // can edit. For the user profile case, there is already an editable contact.
1047 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1048 mHasNewContact = true;
Gary Mai698cee72016-09-19 16:09:54 -07001049 mReadOnlyDisplayNameId = contact.getNameRawContactId();
1050 mCopyReadOnlyName = true;
Walter Jang7b0970f2016-09-01 10:40:19 -07001051 // This is potentially an asynchronous call and will add deltas to list.
1052 selectAccountAndCreateContact();
Walter Jang7b0970f2016-09-01 10:40:19 -07001053 } else {
1054 mHasNewContact = false;
1055 }
1056
Gary Mai698cee72016-09-19 16:09:54 -07001057 setStateForExistingContact(contact.isUserProfile(), mRawContacts);
Gary Maie4874662016-09-26 11:42:54 -07001058 if (mAutoAddToDefaultGroup
1059 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1060 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1061 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001062 }
1063
1064 /**
1065 * Prepare {@link #mState} for a newly created phone-local contact.
1066 */
1067 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1068 boolean isUserProfile) {
1069 setStateForNewContact(account, accountType, /* oldState =*/ null,
1070 /* oldAccountType =*/ null, isUserProfile);
1071 }
1072
1073 /**
1074 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1075 * specified by oldState and oldAccountType.
1076 */
1077 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1078 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1079 mStatus = Status.EDITING;
1080 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1081 mIsUserProfile = isUserProfile;
1082 mNewContactDataReady = true;
1083 bindEditors();
1084 }
1085
1086 /**
1087 * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1088 * {@link #mState}.
1089 *
1090 * If oldState and oldAccountType are specified, the state specified by those parameters
1091 * is migrated to the result {@link RawContactDelta}.
1092 */
1093 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1094 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1095 final RawContact rawContact = new RawContact();
1096 if (account != null) {
1097 rawContact.setAccount(account);
1098 } else {
1099 rawContact.setAccountToLocal();
1100 }
1101
1102 final RawContactDelta result = new RawContactDelta(
1103 ValuesDelta.fromAfter(rawContact.getValues()));
1104 if (oldState == null) {
1105 // Parse any values from incoming intent
1106 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1107 } else {
1108 RawContactModifier.migrateStateForNewContact(
1109 mContext, oldState, result, oldAccountType, accountType);
1110 }
1111
1112 // Ensure we have some default fields (if the account type does not support a field,
1113 // ensureKind will not add it, so it is safe to add e.g. Event)
Gary Mai62ec0b12016-10-07 14:23:54 -07001114 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001115 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1116 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1117 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1118 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1119 RawContactModifier.ensureKindExists(result, accountType,
1120 StructuredPostal.CONTENT_ITEM_TYPE);
1121
1122 // Set the correct URI for saving the contact as a profile
1123 if (mNewLocalProfile) {
1124 result.setProfileQueryUri();
1125 }
1126
1127 return result;
1128 }
1129
1130 /**
1131 * Prepare {@link #mState} for an existing contact.
1132 */
Gary Mai698cee72016-09-19 16:09:54 -07001133 private void setStateForExistingContact(boolean isUserProfile,
Walter Jang7b0970f2016-09-01 10:40:19 -07001134 ImmutableList<RawContact> rawContacts) {
1135 setEnabled(true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001136
1137 mState.addAll(rawContacts.iterator());
1138 setIntentExtras(mIntentExtras);
1139 mIntentExtras = null;
1140
1141 // For user profile, change the contacts query URI
1142 mIsUserProfile = isUserProfile;
1143 boolean localProfileExists = false;
1144
1145 if (mIsUserProfile) {
1146 for (RawContactDelta rawContactDelta : mState) {
1147 // For profile contacts, we need a different query URI
1148 rawContactDelta.setProfileQueryUri();
1149 // Try to find a local profile contact
1150 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1151 localProfileExists = true;
1152 }
1153 }
1154 // Editor should always present a local profile for editing
1155 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
1156 // going to prune all but the one raw contact that we're trying to display by itself.
1157 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1158 mState.add(createLocalRawContactDelta());
1159 }
1160 }
1161 mExistingContactDataReady = true;
1162 bindEditors();
1163 }
1164
1165 /**
1166 * Set the enabled state of editors.
1167 */
1168 private void setEnabled(boolean enabled) {
1169 if (mEnabled != enabled) {
1170 mEnabled = enabled;
1171
1172 // Enable/disable editors
1173 if (mContent != null) {
1174 int count = mContent.getChildCount();
1175 for (int i = 0; i < count; i++) {
1176 mContent.getChildAt(i).setEnabled(enabled);
1177 }
1178 }
1179
Walter Jang7b0970f2016-09-01 10:40:19 -07001180 // Maybe invalidate the options menu
1181 final Activity activity = getActivity();
1182 if (activity != null) activity.invalidateOptionsMenu();
1183 }
1184 }
1185
1186 /**
1187 * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1188 * {@link #mState}.
1189 */
1190 private static RawContactDelta createLocalRawContactDelta() {
1191 final RawContact rawContact = new RawContact();
1192 rawContact.setAccountToLocal();
1193
1194 final RawContactDelta result = new RawContactDelta(
1195 ValuesDelta.fromAfter(rawContact.getValues()));
1196 result.setProfileQueryUri();
1197
1198 return result;
1199 }
1200
Gary Mai698cee72016-09-19 16:09:54 -07001201 private void copyReadOnlyName() {
1202 // We should only ever be doing this if we're creating a new writable contact to attach to
1203 // a read only contact.
1204 if (!isEditingReadOnlyRawContactWithNewContact()) {
1205 return;
1206 }
1207 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1208 final RawContactDelta writable = mState.get(writableIndex);
Gary Mai7b751452016-11-07 17:04:04 -08001209 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
Gary Mai698cee72016-09-19 16:09:54 -07001210 final ValuesDelta writeNameDelta = writable
1211 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1212 final ValuesDelta readNameDelta = readOnly
1213 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1214 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
1215 mCopyReadOnlyName = false;
1216 }
1217
Walter Jang7b0970f2016-09-01 10:40:19 -07001218 /**
1219 * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1220 * Contact.
1221 */
Walter Jangba59deb2015-01-26 11:23:48 -08001222 protected void bindEditors() {
Walter Jangcab3dce2015-02-09 17:48:03 -08001223 if (!isReadyToBindEditors()) {
1224 return;
1225 }
1226
Walter Jangd35e5ef2015-02-24 09:18:16 -08001227 // Add input fields for the loaded Contact
Gary Mai363af602016-09-28 10:01:23 -07001228 final RawContactEditorView editorView = getContent();
Walter Jangb6ca2722015-02-20 11:10:25 -08001229 editorView.setListener(this);
Gary Mai698cee72016-09-19 16:09:54 -07001230 if (mCopyReadOnlyName) {
1231 copyReadOnlyName();
1232 }
Gary Mai678108e2016-10-26 14:34:33 -07001233 editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
Walter Jang9a552372016-08-24 11:51:05 -07001234 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
Gary Mai5a00de32016-10-19 18:20:41 -07001235 mRawContactIdToDisplayAlone);
Gary Mai079598f2016-11-03 15:02:45 -07001236 if (isEditingReadOnlyRawContact()) {
Gary Mai15646ce2016-11-17 10:54:01 -08001237 final Toolbar toolbar = getEditorActivity().getToolbar();
1238 if (toolbar != null) {
1239 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
Gary Maid8f3da62016-11-18 11:47:20 -08001240 // Set activity title for Talkback
1241 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
John Shaobd9ef3c2016-12-15 12:42:03 -08001242 toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
Gary Mai15646ce2016-11-17 10:54:01 -08001243 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
John Shaobd9ef3c2016-12-15 12:42:03 -08001244 toolbar.getNavigationIcon().setAutoMirrored(true);
Gary Mai079598f2016-11-03 15:02:45 -07001245 }
1246 }
Walter Jangcab3dce2015-02-09 17:48:03 -08001247
Walter Jangd35e5ef2015-02-24 09:18:16 -08001248 // Set up the photo widget
Walter Jang31a74ad2015-10-02 19:17:39 -07001249 editorView.setPhotoListener(this);
Walter Jang3efae4a2015-02-18 11:12:00 -08001250 mPhotoRawContactId = editorView.getPhotoRawContactId();
Walter Jang31a74ad2015-10-02 19:17:39 -07001251 // If there is an updated full resolution photo apply it now, this will be the case if
1252 // the user selects or takes a new photo, then rotates the device.
1253 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1254 if (uri != null) {
1255 editorView.setFullSizePhoto(uri);
Walter Jang41b3ea12015-03-09 17:30:06 -07001256 }
Walter Jang3efae4a2015-02-18 11:12:00 -08001257
Walter Jangd35e5ef2015-02-24 09:18:16 -08001258 // The editor is ready now so make it visible
Gary Mai678108e2016-10-26 14:34:33 -07001259 editorView.setEnabled(mEnabled);
Walter Jangd35e5ef2015-02-24 09:18:16 -08001260 editorView.setVisibility(View.VISIBLE);
1261
1262 // Refresh the ActionBar as the visibility of the join command
1263 // Activity can be null if we have been detached from the Activity.
Walter Jangcab3dce2015-02-09 17:48:03 -08001264 invalidateOptionsMenu();
1265 }
1266
Walter Jang7b0970f2016-09-01 10:40:19 -07001267 /**
1268 * Invalidates the options menu if we are still associated with an Activity.
1269 */
1270 private void invalidateOptionsMenu() {
1271 final Activity activity = getActivity();
1272 if (activity != null) {
1273 activity.invalidateOptionsMenu();
1274 }
1275 }
1276
Walter Jangcab3dce2015-02-09 17:48:03 -08001277 private boolean isReadyToBindEditors() {
1278 if (mState.isEmpty()) {
1279 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1280 Log.v(TAG, "No data to bind editors");
1281 }
1282 return false;
1283 }
1284 if (mIsEdit && !mExistingContactDataReady) {
1285 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1286 Log.v(TAG, "Existing contact data is not ready to bind editors.");
1287 }
1288 return false;
1289 }
1290 if (mHasNewContact && !mNewContactDataReady) {
1291 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1292 Log.v(TAG, "New contact data is not ready to bind editors.");
1293 }
1294 return false;
1295 }
1296 return true;
Walter Jangba59deb2015-01-26 11:23:48 -08001297 }
1298
Walter Jang7b0970f2016-09-01 10:40:19 -07001299 /**
1300 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1301 * Some of old data are reused with new restriction enforced by the new account.
1302 *
1303 * @param oldState Old data being edited.
1304 * @param oldAccount Old account associated with oldState.
1305 * @param newAccount New account to be used.
1306 */
1307 private void rebindEditorsForNewContact(
1308 RawContactDelta oldState, AccountWithDataSet oldAccount,
1309 AccountWithDataSet newAccount) {
1310 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1311 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1312 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1313
Gary Maiaebf3202016-09-22 18:11:15 -07001314 mExistingContactDataReady = false;
1315 mNewContactDataReady = false;
1316 mState = new RawContactDeltaList();
1317 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1318 isEditingUserProfile());
1319 if (mIsEdit) {
Gary Mai698cee72016-09-19 16:09:54 -07001320 setStateForExistingContact(isEditingUserProfile(), mRawContacts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001321 }
1322 }
1323
1324 //
1325 // ContactEditor
1326 //
1327
Walter Jang3f990ba2015-01-27 17:38:30 +00001328 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -07001329 public void setListener(Listener listener) {
1330 mListener = listener;
1331 }
1332
1333 @Override
1334 public void load(String action, Uri lookupUri, Bundle intentExtras) {
1335 mAction = action;
1336 mLookupUri = lookupUri;
1337 mIntentExtras = intentExtras;
1338
1339 if (mIntentExtras != null) {
1340 mAutoAddToDefaultGroup =
1341 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1342 mNewLocalProfile =
1343 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1344 mDisableDeleteMenuOption =
1345 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1346 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1347 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1348 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1349 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1350 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1351 }
Gary Maia6c80b32016-09-30 16:34:55 -07001352 mRawContactIdToDisplayAlone = mIntentExtras
1353 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001354 }
1355 }
1356
1357 @Override
1358 public void setIntentExtras(Bundle extras) {
Gary Mai5336e6e2016-10-23 14:17:03 -07001359 getContent().setIntentExtras(extras);
Walter Jang7b0970f2016-09-01 10:40:19 -07001360 }
1361
1362 @Override
1363 public void onJoinCompleted(Uri uri) {
1364 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1365 }
1366
James Laskeye5a140a2016-10-18 15:43:42 -07001367
1368 private String getNameToDisplay(Uri contactUri) {
Gary Maic000d2e2016-11-18 13:51:17 -08001369 // The contact has been deleted or the uri is otherwise no longer right.
1370 if (contactUri == null) {
1371 return null;
1372 }
James Laskeye5a140a2016-10-18 15:43:42 -07001373 final ContentResolver resolver = mContext.getContentResolver();
1374 final Cursor cursor = resolver.query(contactUri, new String[]{
1375 ContactsContract.Contacts.DISPLAY_NAME,
1376 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
James Laskeye5a140a2016-10-18 15:43:42 -07001377
Gary Maia4adae12016-10-23 13:47:17 -07001378 if (cursor != null) {
1379 try {
1380 if (cursor.moveToFirst()) {
1381 final String displayName = cursor.getString(0);
1382 final String displayNameAlt = cursor.getString(1);
1383 cursor.close();
1384 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1385 new ContactsPreferences(mContext));
1386 }
1387 } finally {
1388 cursor.close();
1389 }
1390 }
James Laskeye5a140a2016-10-18 15:43:42 -07001391 return null;
1392 }
1393
1394
Walter Jang7b0970f2016-09-01 10:40:19 -07001395 @Override
1396 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1397 Uri contactLookupUri, Long joinContactId) {
1398 if (hadChanges) {
1399 if (saveSucceeded) {
1400 switch (saveMode) {
1401 case SaveMode.JOIN:
1402 break;
1403 case SaveMode.SPLIT:
1404 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1405 .show();
1406 break;
1407 default:
James Laskeye5a140a2016-10-18 15:43:42 -07001408 final String displayName = getNameToDisplay(contactLookupUri);
James Laskeyb1671052016-09-16 13:57:21 -07001409 final String toastMessage;
1410 if (!TextUtils.isEmpty(displayName)) {
1411 toastMessage = getResources().getString(
1412 R.string.contactSavedNamedToast, displayName);
1413 } else {
1414 toastMessage = getResources().getString(R.string.contactSavedToast);
1415 }
1416 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
Walter Jang7b0970f2016-09-01 10:40:19 -07001417 }
1418
1419 } else {
1420 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1421 }
1422 }
1423 switch (saveMode) {
1424 case SaveMode.CLOSE: {
Walter Jang581585d2016-09-21 19:21:13 -07001425 Intent resultIntent = null;
Walter Jang7b0970f2016-09-01 10:40:19 -07001426 if (saveSucceeded && contactLookupUri != null) {
1427 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1428 mContext, contactLookupUri, mLookupUri);
Walter Jangdf86ede2016-10-19 09:48:29 -07001429 if (Flags.getInstance().getBoolean(Experiments.CONTACT_SHEET)) {
Walter Jang581585d2016-09-21 19:21:13 -07001430 resultIntent = ObjectFactory.getContactSheetIntent(mContext, lookupUri);
1431 }
1432 if (resultIntent == null) {
1433 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1434 mContext, lookupUri, ScreenType.EDITOR);
1435 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
1436 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001437 } else {
1438 resultIntent = null;
1439 }
1440 // It is already saved, so prevent it from being saved again
1441 mStatus = Status.CLOSING;
1442 if (mListener != null) mListener.onSaveFinished(resultIntent);
1443 break;
1444 }
Gary Mai363af602016-09-28 10:01:23 -07001445 case SaveMode.EDITOR: {
Walter Jang7b0970f2016-09-01 10:40:19 -07001446 // It is already saved, so prevent it from being saved again
1447 mStatus = Status.CLOSING;
1448 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1449 break;
1450 }
1451 case SaveMode.JOIN:
1452 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1453 joinAggregate(joinContactId);
1454 }
1455 break;
1456 case SaveMode.RELOAD:
1457 if (saveSucceeded && contactLookupUri != null) {
1458 // If this was in INSERT, we are changing into an EDIT now.
1459 // If it already was an EDIT, we are changing to the new Uri now
1460 mState = new RawContactDeltaList();
1461 load(Intent.ACTION_EDIT, contactLookupUri, null);
1462 mStatus = Status.LOADING;
1463 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1464 }
1465 break;
1466
1467 case SaveMode.SPLIT:
1468 mStatus = Status.CLOSING;
1469 if (mListener != null) {
1470 mListener.onContactSplit(contactLookupUri);
1471 } else {
1472 Log.d(TAG, "No listener registered, can not call onSplitFinished");
1473 }
1474 break;
1475 }
1476 }
1477
1478 /**
1479 * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1480 *
1481 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1482 */
1483 private void showJoinAggregateActivity(Uri contactLookupUri) {
1484 if (contactLookupUri == null || !isAdded()) {
1485 return;
1486 }
1487
1488 mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1489 final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1490 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1491 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1492 startActivityForResult(intent, REQUEST_CODE_JOIN);
1493 }
1494
1495 //
1496 // Aggregation PopupWindow
1497 //
1498
1499 /**
1500 * Triggers an asynchronous search for aggregation suggestions.
1501 */
1502 protected void acquireAggregationSuggestions(Context context,
1503 long rawContactId, ValuesDelta valuesDelta) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001504 mAggregationSuggestionsRawContactId = rawContactId;
1505
1506 if (mAggregationSuggestionEngine == null) {
1507 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1508 mAggregationSuggestionEngine.setListener(this);
1509 mAggregationSuggestionEngine.start();
1510 }
1511
1512 mAggregationSuggestionEngine.setContactId(getContactId());
Gary Mai220d10c2016-09-23 13:56:39 -07001513 mAggregationSuggestionEngine.setAccountFilter(
1514 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
Walter Jang7b0970f2016-09-01 10:40:19 -07001515
1516 mAggregationSuggestionEngine.onNameChange(valuesDelta);
1517 }
1518
1519 /**
1520 * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1521 */
1522 private long getContactId() {
1523 for (RawContactDelta rawContact : mState) {
1524 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1525 if (contactId != null) {
1526 return contactId;
1527 }
1528 }
1529 return 0;
1530 }
1531
1532 @Override
1533 public void onAggregationSuggestionChange() {
1534 final Activity activity = getActivity();
1535 if ((activity != null && activity.isFinishing())
1536 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
1537 return;
1538 }
1539
1540 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1541
1542 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1543 return;
1544 }
1545
Gary Maida20b472016-09-20 14:46:40 -07001546 final View anchorView = getAggregationAnchorView();
Walter Jang7b0970f2016-09-01 10:40:19 -07001547 if (anchorView == null) {
1548 return; // Raw contact deleted?
1549 }
1550 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1551 mAggregationSuggestionPopup.setAnchorView(anchorView);
1552 mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1553 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1554 mAggregationSuggestionPopup.setAdapter(
1555 new AggregationSuggestionAdapter(
1556 getActivity(),
Walter Jang7b0970f2016-09-01 10:40:19 -07001557 /* listener =*/ this,
1558 mAggregationSuggestionEngine.getSuggestions()));
1559 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1560 @Override
1561 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1562 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1563 suggestionView.handleItemClickEvent();
1564 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1565 mAggregationSuggestionPopup = null;
1566 }
1567 });
1568 mAggregationSuggestionPopup.show();
1569 }
1570
1571 /**
Gary Maida20b472016-09-20 14:46:40 -07001572 * Returns the editor view that should be used as the anchor for aggregation suggestions.
Walter Jang7b0970f2016-09-01 10:40:19 -07001573 */
Gary Maida20b472016-09-20 14:46:40 -07001574 protected View getAggregationAnchorView() {
Walter Jangd35e5ef2015-02-24 09:18:16 -08001575 return getContent().getAggregationAnchorView();
1576 }
1577
Walter Jang7b0970f2016-09-01 10:40:19 -07001578 /**
1579 * Joins the suggested contact (specified by the id's of constituent raw
1580 * contacts), save all changes, and stay in the editor.
1581 */
1582 public void doJoinSuggestedContact(long[] rawContactIds) {
1583 if (!hasValidState() || mStatus != Status.EDITING) {
1584 return;
1585 }
1586
1587 mState.setJoinWithRawContacts(rawContactIds);
1588 save(SaveMode.RELOAD);
1589 }
1590
1591 @Override
Gary Mai678108e2016-10-26 14:34:33 -07001592 public void onEditAction(Uri contactLookupUri, long rawContactId) {
1593 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
Walter Jang7b0970f2016-09-01 10:40:19 -07001594 }
1595
1596 /**
Gary Mai678108e2016-10-26 14:34:33 -07001597 * Abandons the currently edited contact and switches to editing the selected raw contact,
1598 * transferring all the data there
Walter Jang7b0970f2016-09-01 10:40:19 -07001599 */
Gary Mai678108e2016-10-26 14:34:33 -07001600 public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001601 if (mListener != null) {
1602 // make sure we don't save this contact when closing down
1603 mStatus = Status.CLOSING;
Gary Mai678108e2016-10-26 14:34:33 -07001604 mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1605 getContent().getCurrentRawContactDelta().getContentValues());
Walter Jang7b0970f2016-09-01 10:40:19 -07001606 }
1607 }
1608
1609 /**
1610 * Sets group metadata on all bound editors.
1611 */
Walter Jang92f8ccc2015-02-06 10:23:37 -08001612 protected void setGroupMetaData() {
Walter Jangf10ca152015-09-22 15:23:55 -07001613 if (mGroupMetaData != null) {
1614 getContent().setGroupMetaData(mGroupMetaData);
1615 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001616 }
1617
Walter Jang7b0970f2016-09-01 10:40:19 -07001618 /**
1619 * Persist the accumulated editor deltas.
1620 *
1621 * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1622 * may be null.
1623 */
Walter Jange3373dc2015-10-27 15:35:12 -07001624 protected boolean doSaveAction(int saveMode, Long joinContactId) {
Walter Jang49ed2032015-02-11 20:09:05 -08001625 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1626 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1627 ((Activity) mContext).getClass(),
Gary Mai363af602016-09-28 10:01:23 -07001628 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
Walter Jange3373dc2015-10-27 15:35:12 -07001629 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
Wenyi Wangdd7d4562015-12-08 13:33:43 -08001630 return startSaveService(mContext, intent, saveMode);
Walter Jang49ed2032015-02-11 20:09:05 -08001631 }
1632
Walter Jang7b0970f2016-09-01 10:40:19 -07001633 private boolean startSaveService(Context context, Intent intent, int saveMode) {
1634 final boolean result = ContactSaveService.startService(
1635 context, intent, saveMode);
1636 if (!result) {
1637 onCancelEditConfirmed();
1638 }
1639 return result;
1640 }
1641
1642 //
1643 // Join Activity
1644 //
1645
1646 /**
1647 * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1648 */
Walter Jang49ed2032015-02-11 20:09:05 -08001649 protected void joinAggregate(final long contactId) {
1650 final Intent intent = ContactSaveService.createJoinContactsIntent(
Gary Mai363af602016-09-28 10:01:23 -07001651 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1652 ContactEditorActivity.ACTION_JOIN_COMPLETED);
Walter Jang49ed2032015-02-11 20:09:05 -08001653 mContext.startService(intent);
Walter Jang3f990ba2015-01-27 17:38:30 +00001654 }
Walter Jangb6ca2722015-02-20 11:10:25 -08001655
Walter Jang31a74ad2015-10-02 19:17:39 -07001656 public void removePhoto() {
1657 getContent().removePhoto();
1658 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
Walter Jang0e72ce92015-02-23 12:27:21 -08001659 }
1660
Walter Jang31a74ad2015-10-02 19:17:39 -07001661 public void updatePhoto(Uri uri) throws FileNotFoundException {
1662 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1663 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Wenyi Wang9bc9ba82015-11-17 19:37:33 -08001664 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Walter Jang31a74ad2015-10-02 19:17:39 -07001665 Toast.LENGTH_SHORT).show();
1666 return;
Walter Jang0e72ce92015-02-23 12:27:21 -08001667 }
Walter Jang31a74ad2015-10-02 19:17:39 -07001668 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1669 getContent().updatePhoto(uri);
Walter Jang0e72ce92015-02-23 12:27:21 -08001670 }
1671
Gary Maida20b472016-09-20 14:46:40 -07001672 public void setPrimaryPhoto() {
1673 getContent().setPrimaryPhoto();
Walter Jang0e72ce92015-02-23 12:27:21 -08001674 }
1675
1676 @Override
Walter Jang151f3e62015-02-26 15:29:40 -08001677 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1678 final Activity activity = getActivity();
1679 if (activity == null || activity.isFinishing()) {
1680 return;
1681 }
Walter Jang45b86d52015-10-15 15:23:16 -07001682 acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
Walter Jang151f3e62015-02-26 15:29:40 -08001683 }
1684
Walter Jang5a7a23b2015-03-06 10:54:26 -08001685 @Override
Walter Jang708ea9e2015-09-10 15:42:05 -07001686 public void onRebindEditorsForNewContact(RawContactDelta oldState,
1687 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1688 mNewContactAccountChanged = true;
1689 mAccountWithDataSet = newAccount;
1690 rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1691 }
1692
Walter Jang79658e12015-09-24 10:36:26 -07001693 @Override
1694 public void onBindEditorsFailed() {
1695 final Activity activity = getActivity();
1696 if (activity != null && !activity.isFinishing()) {
Gary Mai363af602016-09-28 10:01:23 -07001697 Toast.makeText(activity, R.string.editor_failed_to_load,
Walter Jang79658e12015-09-24 10:36:26 -07001698 Toast.LENGTH_SHORT).show();
1699 activity.setResult(Activity.RESULT_CANCELED);
1700 activity.finish();
1701 }
1702 }
1703
Walter Jangd6753152015-10-02 09:23:13 -07001704 @Override
1705 public void onEditorsBound() {
Wenyi Wang3cb77bb2016-07-27 17:39:03 -07001706 final Activity activity = getActivity();
1707 if (activity == null || activity.isFinishing()) {
1708 return;
1709 }
Walter Jangd6753152015-10-02 09:23:13 -07001710 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1711 }
1712
Walter Jang31a74ad2015-10-02 19:17:39 -07001713 @Override
1714 public void onPhotoEditorViewClicked() {
Walter Jang3f18d612015-10-07 16:01:05 -07001715 // For contacts composed of a single writable raw contact, or raw contacts have no more
1716 // than 1 photo, clicking the photo view simply opens the source photo dialog
Walter Jang31a74ad2015-10-02 19:17:39 -07001717 getEditorActivity().changePhoto(getPhotoMode());
1718 }
1719
1720 private int getPhotoMode() {
Gary Maida20b472016-09-20 14:46:40 -07001721 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1722 : PhotoActionPopup.Modes.NO_PHOTO;
Walter Jang31a74ad2015-10-02 19:17:39 -07001723 }
1724
Gary Mai363af602016-09-28 10:01:23 -07001725 private ContactEditorActivity getEditorActivity() {
1726 return (ContactEditorActivity) getActivity();
Walter Jang31a74ad2015-10-02 19:17:39 -07001727 }
1728
Gary Mai363af602016-09-28 10:01:23 -07001729 private RawContactEditorView getContent() {
1730 return (RawContactEditorView) mContent;
Walter Jang3efae4a2015-02-18 11:12:00 -08001731 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001732}