blob: 8f8d10fcbd6ec9949cd6ace7fd8703a0243ba9a4 [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;
60import com.android.contacts.GroupMetaDataLoader;
61import com.android.contacts.R;
Gary Maia4adae12016-10-23 13:47:17 -070062import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
Gary Mai363af602016-09-28 10:01:23 -070063import com.android.contacts.activities.ContactEditorActivity;
64import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
Walter Jang7b0970f2016-09-01 10:40:19 -070065import com.android.contacts.activities.ContactSelectionActivity;
Walter Jang581585d2016-09-21 19:21:13 -070066import com.android.contacts.common.Experiments;
Walter Jang7b0970f2016-09-01 10:40:19 -070067import com.android.contacts.common.logging.ScreenEvent.ScreenType;
68import com.android.contacts.common.model.AccountTypeManager;
69import com.android.contacts.common.model.Contact;
70import com.android.contacts.common.model.ContactLoader;
71import com.android.contacts.common.model.RawContact;
72import com.android.contacts.common.model.RawContactDelta;
73import com.android.contacts.common.model.RawContactDeltaList;
74import com.android.contacts.common.model.RawContactModifier;
75import com.android.contacts.common.model.ValuesDelta;
76import com.android.contacts.common.model.account.AccountType;
77import com.android.contacts.common.model.account.AccountWithDataSet;
James Laskeye5a140a2016-10-18 15:43:42 -070078import com.android.contacts.common.preference.ContactsPreferences;
79import com.android.contacts.common.util.ContactDisplayUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070080import com.android.contacts.common.util.ImplicitIntentsUtil;
81import com.android.contacts.common.util.MaterialColorMapUtils;
82import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
Gary Mai5c1bff22016-09-30 15:10:25 -070083import com.android.contacts.group.GroupUtil;
Walter Jang7b0970f2016-09-01 10:40:19 -070084import com.android.contacts.list.UiIntentActions;
Gary Maie4874662016-09-26 11:42:54 -070085import com.android.contacts.quickcontact.InvisibleContactUtil;
Walter Jang7b0970f2016-09-01 10:40:19 -070086import com.android.contacts.quickcontact.QuickContactActivity;
87import com.android.contacts.util.ContactPhotoUtils;
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;
305 protected View mAggregationSuggestionView;
306 protected ListPopupWindow mAggregationSuggestionPopup;
307
308 //
309 // Parameters passed in on {@link #load}
310 //
311 protected String mAction;
312 protected Uri mLookupUri;
313 protected Bundle mIntentExtras;
314 protected boolean mAutoAddToDefaultGroup;
315 protected boolean mDisableDeleteMenuOption;
316 protected boolean mNewLocalProfile;
317 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang7b0970f2016-09-01 10:40:19 -0700318
319 //
320 // Helpers
321 //
322 protected ContactEditorUtils mEditorUtils;
323 protected RawContactDeltaComparator mComparator;
324 protected ViewIdGenerator mViewIdGenerator;
325 private AggregationSuggestionEngine mAggregationSuggestionEngine;
326
327 //
328 // Loaded data
329 //
330 // Used to store existing contact data so it can be re-applied during a rebind call,
331 // i.e. account switch.
Gary Mai7b751452016-11-07 17:04:04 -0800332 protected Contact mContact;
Walter Jang7b0970f2016-09-01 10:40:19 -0700333 protected ImmutableList<RawContact> mRawContacts;
334 protected Cursor mGroupMetaData;
335
336 //
337 // Editor state
338 //
339 protected RawContactDeltaList mState;
340 protected int mStatus;
341 protected long mRawContactIdToDisplayAlone = -1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700342
343 // Whether to show the new contact blank form and if it's corresponding delta is ready.
344 protected boolean mHasNewContact;
345 protected AccountWithDataSet mAccountWithDataSet;
346 protected boolean mNewContactDataReady;
347 protected boolean mNewContactAccountChanged;
348
349 // Whether it's an edit of existing contact and if it's corresponding delta is ready.
350 protected boolean mIsEdit;
351 protected boolean mExistingContactDataReady;
352
353 // Whether we are editing the "me" profile
354 protected boolean mIsUserProfile;
355
Walter Jang7b0970f2016-09-01 10:40:19 -0700356 // Whether editor views and options menu items should be enabled
357 private boolean mEnabled = true;
358
359 // Aggregation PopupWindow
360 private long mAggregationSuggestionsRawContactId;
361
362 // Join Activity
363 protected long mContactIdForJoin;
364
365 // 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 -0700366 protected long mReadOnlyDisplayNameId;
367 protected boolean mCopyReadOnlyName;
Walter Jang7b0970f2016-09-01 10:40:19 -0700368
369 /**
370 * The contact data loader listener.
371 */
372 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
373 new LoaderManager.LoaderCallbacks<Contact>() {
374
375 protected long mLoaderStartTime;
376
377 @Override
378 public Loader<Contact> onCreateLoader(int id, Bundle args) {
379 mLoaderStartTime = SystemClock.elapsedRealtime();
Gary Maie4874662016-09-26 11:42:54 -0700380 return new ContactLoader(mContext, mLookupUri,
381 /* postViewNotification */ true,
382 /* loadGroupMetaData */ true);
Walter Jang7b0970f2016-09-01 10:40:19 -0700383 }
384
385 @Override
386 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
387 final long loaderCurrentTime = SystemClock.elapsedRealtime();
388 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
389 if (!contact.isLoaded()) {
390 // Item has been deleted. Close activity without saving again.
391 Log.i(TAG, "No contact found. Closing activity");
392 mStatus = Status.CLOSING;
393 if (mListener != null) mListener.onContactNotFound();
394 return;
395 }
396
397 mStatus = Status.EDITING;
398 mLookupUri = contact.getLookupUri();
399 final long setDataStartTime = SystemClock.elapsedRealtime();
400 setState(contact);
Walter Jang7b0970f2016-09-01 10:40:19 -0700401 final long setDataEndTime = SystemClock.elapsedRealtime();
402
403 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
404 }
405
406 @Override
407 public void onLoaderReset(Loader<Contact> loader) {
408 }
409 };
410
411 /**
412 * The groups meta data loader listener.
413 */
414 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
415 new LoaderManager.LoaderCallbacks<Cursor>() {
416
417 @Override
418 public CursorLoader onCreateLoader(int id, Bundle args) {
Gary Mai5c1bff22016-09-30 15:10:25 -0700419 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
420 GroupUtil.ALL_GROUPS_SELECTION);
Walter Jang7b0970f2016-09-01 10:40:19 -0700421 }
422
423 @Override
424 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
425 mGroupMetaData = data;
426 setGroupMetaData();
427 }
428
429 @Override
430 public void onLoaderReset(Loader<Cursor> loader) {
431 }
432 };
433
Walter Jang3efae4a2015-02-18 11:12:00 -0800434 private long mPhotoRawContactId;
Walter Jang28a27272015-09-19 16:06:08 -0700435 private Bundle mUpdatedPhotos = new Bundle();
Walter Jang3efae4a2015-02-18 11:12:00 -0800436
437 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700438 public Context getContext() {
439 return getActivity();
440 }
441
442 @Override
443 public void onAttach(Activity activity) {
444 super.onAttach(activity);
445 mContext = activity;
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700446 mEditorUtils = ContactEditorUtils.create(mContext);
Walter Jang7b0970f2016-09-01 10:40:19 -0700447 mComparator = new RawContactDeltaComparator(mContext);
448 }
449
450 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800451 public void onCreate(Bundle savedState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700452 if (savedState != null) {
453 // Restore mUri before calling super.onCreate so that onInitializeLoaders
454 // would already have a uri and an action to work with
455 mAction = savedState.getString(KEY_ACTION);
456 mLookupUri = savedState.getParcelable(KEY_URI);
457 }
458
Walter Jang3efae4a2015-02-18 11:12:00 -0800459 super.onCreate(savedState);
460
Walter Jang7b0970f2016-09-01 10:40:19 -0700461 if (savedState == null) {
462 mViewIdGenerator = new ViewIdGenerator();
463
464 // mState can still be null because it may not have have finished loading before
465 // onSaveInstanceState was called.
466 mState = new RawContactDeltaList();
467 } else {
468 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
469
470 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
471 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
472 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
473 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
Gary Maic135a5d2016-12-19 11:13:46 -0800474 mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
Walter Jang7b0970f2016-09-01 10:40:19 -0700475 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
476 KEY_RAW_CONTACTS));
477 // NOTE: mGroupMetaData is not saved/restored
478
479 // Read state from savedState. No loading involved here
480 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
481 mStatus = savedState.getInt(KEY_STATUS);
Walter Jang7b0970f2016-09-01 10:40:19 -0700482
483 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
484 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
485
486 mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
487 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
488
489 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
490
Walter Jang7b0970f2016-09-01 10:40:19 -0700491 mEnabled = savedState.getBoolean(KEY_ENABLED);
492
493 // Aggregation PopupWindow
494 mAggregationSuggestionsRawContactId = savedState.getLong(
495 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
496
497 // Join Activity
498 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
499
Gary Mai698cee72016-09-19 16:09:54 -0700500 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
501 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700502
Walter Jang3efae4a2015-02-18 11:12:00 -0800503 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
Walter Jang28a27272015-09-19 16:06:08 -0700504 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
Walter Jang3efae4a2015-02-18 11:12:00 -0800505 }
506 }
507
Walter Jang3f990ba2015-01-27 17:38:30 +0000508 @Override
Walter Jang3f990ba2015-01-27 17:38:30 +0000509 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Walter Janged8f6c92015-01-30 16:07:47 -0800510 setHasOptionsMenu(true);
511
Walter Jang3f990ba2015-01-27 17:38:30 +0000512 final View view = inflater.inflate(
Gary Mai363af602016-09-28 10:01:23 -0700513 R.layout.contact_editor_fragment, container, false);
Walter Jangf5dfea42015-09-16 12:30:36 -0700514 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
Walter Jang3f990ba2015-01-27 17:38:30 +0000515 return view;
516 }
517
Walter Janged8f6c92015-01-30 16:07:47 -0800518 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700519 public void onActivityCreated(Bundle savedInstanceState) {
520 super.onActivityCreated(savedInstanceState);
521
522 validateAction(mAction);
523
524 if (mState.isEmpty()) {
525 // The delta list may not have finished loading before orientation change happens.
526 // In this case, there will be a saved state but deltas will be missing. Reload from
527 // database.
528 if (Intent.ACTION_EDIT.equals(mAction)) {
529 // Either
530 // 1) orientation change but load never finished.
531 // 2) not an orientation change so data needs to be loaded for first time.
532 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
533 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
534 }
535 } else {
536 // Orientation change, we already have mState, it was loaded by onCreate
537 bindEditors();
538 }
539
540 // Handle initial actions only when existing state missing
541 if (savedInstanceState == null) {
542 final Account account = mIntentExtras == null ? null :
543 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
544 final String dataSet = mIntentExtras == null ? null :
545 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
546 if (account != null) {
547 mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
548 }
549
550 if (Intent.ACTION_EDIT.equals(mAction)) {
551 mIsEdit = true;
552 } else if (Intent.ACTION_INSERT.equals(mAction)) {
553 mHasNewContact = true;
554 if (mAccountWithDataSet != null) {
555 createContact(mAccountWithDataSet);
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700556 } else if (mIntentExtras != null && mIntentExtras.getBoolean(
Gary Mai363af602016-09-28 10:01:23 -0700557 ContactEditorActivity.EXTRA_SAVE_TO_DEVICE_FLAG, false)) {
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700558 createContact(null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700559 } else {
560 // No Account specified. Let the user choose
561 // Load Accounts async so that we can present them
562 selectAccountAndCreateContact();
563 }
564 }
565 }
566 }
567
568 /**
569 * Checks if the requested action is valid.
570 *
571 * @param action The action to test.
572 * @throws IllegalArgumentException when the action is invalid.
573 */
574 private static void validateAction(String action) {
575 if (VALID_INTENT_ACTIONS.contains(action)) {
576 return;
577 }
578 throw new IllegalArgumentException(
579 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
580 }
581
582 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800583 public void onSaveInstanceState(Bundle outState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700584 outState.putString(KEY_ACTION, mAction);
585 outState.putParcelable(KEY_URI, mLookupUri);
586 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
587 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
588 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
589 if (mMaterialPalette != null) {
590 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
591 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700592 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
593
594 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
595 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
596 // NOTE: mGroupMetaData is not saved
597
Gary Mai36ceb422016-10-17 14:04:17 -0700598 outState.putParcelable(KEY_EDIT_STATE, mState);
Walter Jang7b0970f2016-09-01 10:40:19 -0700599 outState.putInt(KEY_STATUS, mStatus);
600 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
601 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
602 outState.putBoolean(KEY_IS_EDIT, mIsEdit);
603 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
Gary Maic135a5d2016-12-19 11:13:46 -0800604 outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
Walter Jang7b0970f2016-09-01 10:40:19 -0700605 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
606
Walter Jang7b0970f2016-09-01 10:40:19 -0700607 outState.putBoolean(KEY_ENABLED, mEnabled);
608
609 // Aggregation PopupWindow
610 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
611 mAggregationSuggestionsRawContactId);
612
613 // Join Activity
614 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
615
Gary Mai698cee72016-09-19 16:09:54 -0700616 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
617 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
Walter Jang7b0970f2016-09-01 10:40:19 -0700618
Walter Jang3efae4a2015-02-18 11:12:00 -0800619 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
Walter Jang28a27272015-09-19 16:06:08 -0700620 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
Walter Jang3efae4a2015-02-18 11:12:00 -0800621 super.onSaveInstanceState(outState);
622 }
623
624 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700625 public void onStop() {
626 super.onStop();
627 UiClosables.closeQuietly(mAggregationSuggestionPopup);
628 }
629
630 @Override
631 public void onDestroy() {
632 super.onDestroy();
633 if (mAggregationSuggestionEngine != null) {
634 mAggregationSuggestionEngine.quit();
635 }
636 }
637
638 @Override
639 public void onActivityResult(int requestCode, int resultCode, Intent data) {
640 switch (requestCode) {
641 case REQUEST_CODE_JOIN: {
642 // Ignore failed requests
643 if (resultCode != Activity.RESULT_OK) return;
644 if (data != null) {
645 final long contactId = ContentUris.parseId(data.getData());
646 if (hasPendingChanges()) {
647 // Ask the user if they want to save changes before doing the join
648 JoinContactConfirmationDialogFragment.show(this, contactId);
649 } else {
650 // Do the join immediately
651 joinAggregate(contactId);
652 }
653 }
654 break;
655 }
656 case REQUEST_CODE_ACCOUNTS_CHANGED: {
657 // Bail if the account selector was not successful.
658 if (resultCode != Activity.RESULT_OK) {
659 if (mListener != null) {
660 mListener.onReverted();
661 }
662 return;
663 }
664 // If there's an account specified, use it.
665 if (data != null) {
666 AccountWithDataSet account = data.getParcelableExtra(
667 Intents.Insert.EXTRA_ACCOUNT);
668 if (account != null) {
669 createContact(account);
670 return;
671 }
672 }
673 // If there isn't an account specified, then this is likely a phone-local
674 // contact, so we should continue setting up the editor by automatically selecting
675 // the most appropriate account.
676 createContact();
677 break;
678 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700679 }
680 }
681
Walter Jang7b0970f2016-09-01 10:40:19 -0700682 //
683 // Options menu
684 //
685
Walter Jang7b0970f2016-09-01 10:40:19 -0700686 @Override
687 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
688 inflater.inflate(R.menu.edit_contact, menu);
689 }
690
691 @Override
692 public void onPrepareOptionsMenu(Menu menu) {
693 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
694 // because the custom action bar contains the "save" button now (not the overflow menu).
695 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
696 final MenuItem saveMenu = menu.findItem(R.id.menu_save);
697 final MenuItem splitMenu = menu.findItem(R.id.menu_split);
698 final MenuItem joinMenu = menu.findItem(R.id.menu_join);
Walter Jang7b0970f2016-09-01 10:40:19 -0700699 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
700
Gary Mai5eda2572016-10-11 18:01:32 -0700701 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
702 // on a raw contact level.
703 joinMenu.setVisible(false);
704 splitMenu.setVisible(false);
705 deleteMenu.setVisible(false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700706 // Save menu is invisible when there's only one read only contact in the editor.
Gary Maid7faa652016-10-03 11:53:39 -0700707 saveMenu.setVisible(!isEditingReadOnlyRawContact());
Walter Jang7b0970f2016-09-01 10:40:19 -0700708 if (saveMenu.isVisible()) {
709 // Since we're using a custom action layout we have to manually hook up the handler.
710 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
711 @Override
712 public void onClick(View v) {
713 onOptionsItemSelected(saveMenu);
714 }
715 });
716 }
717
Walter Jang7b0970f2016-09-01 10:40:19 -0700718 int size = menu.size();
719 for (int i = 0; i < size; i++) {
720 menu.getItem(i).setEnabled(mEnabled);
721 }
722 }
723
724 @Override
Walter Jangc90cc152015-06-19 14:15:08 -0700725 public boolean onOptionsItemSelected(MenuItem item) {
726 if (item.getItemId() == android.R.id.home) {
727 return revert();
728 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700729
730 final Activity activity = getActivity();
731 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
732 // If we no longer are attached to a running activity want to
733 // drain this event.
734 return true;
735 }
736
737 switch (item.getItemId()) {
738 case R.id.menu_save:
739 return save(SaveMode.CLOSE);
740 case R.id.menu_delete:
741 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
742 return true;
743 case R.id.menu_split:
744 return doSplitContactAction();
745 case R.id.menu_join:
746 return doJoinContactAction();
Gary Maia4adae12016-10-23 13:47:17 -0700747 case R.id.menu_help:
748 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
Walter Jang7b0970f2016-09-01 10:40:19 -0700749 return true;
750 }
751
752 return false;
Walter Jangc90cc152015-06-19 14:15:08 -0700753 }
754
755 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700756 public boolean revert() {
757 if (mState.isEmpty() || !hasPendingChanges()) {
758 onCancelEditConfirmed();
759 } else {
760 CancelEditDialogFragment.show(this);
761 }
762 return true;
763 }
764
765 @Override
766 public void onCancelEditConfirmed() {
767 // When this Fragment is closed we don't want it to auto-save
768 mStatus = Status.CLOSING;
769 if (mListener != null) {
770 mListener.onReverted();
771 }
772 }
773
774 @Override
775 public void onSplitContactConfirmed(boolean hasPendingChanges) {
776 if (mState.isEmpty()) {
777 // This may happen when this Fragment is recreated by the system during users
778 // confirming the split action (and thus this method is called just before onCreate()),
779 // for example.
780 Log.e(TAG, "mState became null during the user's confirming split action. " +
781 "Cannot perform the save action.");
782 return;
783 }
784
785 if (!hasPendingChanges && mHasNewContact) {
786 // If the user didn't add anything new, we don't want to split out the newly created
787 // raw contact into a name-only contact so remove them.
788 final Iterator<RawContactDelta> iterator = mState.iterator();
789 while (iterator.hasNext()) {
790 final RawContactDelta rawContactDelta = iterator.next();
791 if (rawContactDelta.getRawContactId() < 0) {
792 iterator.remove();
793 }
794 }
795 }
796 mState.markRawContactsForSplitting();
797 save(SaveMode.SPLIT);
798 }
799
Gary Maib9065dd2016-11-08 10:49:00 -0800800 @Override
801 public void onSplitContactCanceled() {}
802
Walter Jang7b0970f2016-09-01 10:40:19 -0700803 private boolean doSplitContactAction() {
804 if (!hasValidState()) return false;
805
806 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
807 return true;
808 }
809
810 private boolean doJoinContactAction() {
811 if (!hasValidState() || mLookupUri == null) {
812 return false;
813 }
814
815 // If we just started creating a new contact and haven't added any data, it's too
816 // early to do a join
817 if (mState.size() == 1 && mState.get(0).isContactInsert()
818 && !hasPendingChanges()) {
819 Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
820 Toast.LENGTH_LONG).show();
821 return true;
822 }
823
824 showJoinAggregateActivity(mLookupUri);
825 return true;
826 }
827
828 @Override
829 public void onJoinContactConfirmed(long joinContactId) {
830 doSaveAction(SaveMode.JOIN, joinContactId);
831 }
832
Walter Jang7b0970f2016-09-01 10:40:19 -0700833 @Override
834 public boolean save(int saveMode) {
835 if (!hasValidState() || mStatus != Status.EDITING) {
836 return false;
837 }
838
839 // If we are about to close the editor - there is no need to refresh the data
Gary Mai363af602016-09-28 10:01:23 -0700840 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
Walter Jang7b0970f2016-09-01 10:40:19 -0700841 || saveMode == SaveMode.SPLIT) {
842 getLoaderManager().destroyLoader(LOADER_CONTACT);
843 }
844
845 mStatus = Status.SAVING;
846
847 if (!hasPendingChanges()) {
848 if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
849 // We don't have anything to save and there isn't even an existing contact yet.
850 // Nothing to do, simply go back to editing mode
851 mStatus = Status.EDITING;
852 return true;
853 }
854 onSaveCompleted(/* hadChanges =*/ false, saveMode,
855 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
856 return true;
857 }
858
859 setEnabled(false);
860
861 return doSaveAction(saveMode, /* joinContactId */ null);
862 }
863
864 //
865 // State accessor methods
866 //
867
868 /**
869 * Check if our internal {@link #mState} is valid, usually checked before
870 * performing user actions.
871 */
872 private boolean hasValidState() {
873 return mState.size() > 0;
874 }
875
876 private boolean isEditingUserProfile() {
877 return mNewLocalProfile || mIsUserProfile;
878 }
879
880 /**
Gary Mai5a00de32016-10-19 18:20:41 -0700881 * Whether the contact being edited is composed of read-only raw contacts
Walter Jang7b0970f2016-09-01 10:40:19 -0700882 * aggregated with a newly created writable raw contact.
883 */
884 private boolean isEditingReadOnlyRawContactWithNewContact() {
Gary Mai5a00de32016-10-19 18:20:41 -0700885 return mHasNewContact && mState.size() > 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700886 }
887
888 /**
Gary Maid7faa652016-10-03 11:53:39 -0700889 * @return true if the single raw contact we're looking at is read-only.
890 */
891 private boolean isEditingReadOnlyRawContact() {
892 return hasValidState() && mRawContactIdToDisplayAlone > 0
893 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
894 .getAccountType(AccountTypeManager.getInstance(mContext))
895 .areContactsWritable();
896 }
897
898 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700899 * Return true if there are any edits to the current contact which need to
900 * be saved.
901 */
902 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
903 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
904 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
905 }
906
907 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700908 * Determines if changes were made in the editor that need to be saved, while taking into
909 * account that name changes are not real for read-only contacts.
910 * See go/editing-read-only-contacts
911 */
912 private boolean hasPendingChanges() {
Gary Mai698cee72016-09-19 16:09:54 -0700913 if (isEditingReadOnlyRawContactWithNewContact()) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700914 // We created a new raw contact delta with a default display name.
915 // We must test for pending changes while ignoring the default display name.
Gary Mai698cee72016-09-19 16:09:54 -0700916 final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId)
917 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
918 final ValuesDelta pendingDelta = mState
919 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
920 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700921 final Set<String> excludedMimeTypes = new HashSet<>();
922 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
923 return hasPendingRawContactChanges(excludedMimeTypes);
924 }
925 return true;
926 }
927 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
928 }
929
930 /**
Gary Mai698cee72016-09-19 16:09:54 -0700931 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
932 * of a read only delta and now we want to check if the copied delta has changes.
933 *
934 * @param before original {@link ValuesDelta}
935 * @param after copied {@link ValuesDelta}
936 * @return true if the copied {@link ValuesDelta} has all the same values in the structured
937 * name fields as the original.
938 */
939 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
Gary Mai5a00de32016-10-19 18:20:41 -0700940 if (before == after) return true;
Gary Mai698cee72016-09-19 16:09:54 -0700941 if (before == null || after == null) return false;
942 final ContentValues original = before.getBefore();
943 final ContentValues pending = after.getAfter();
944 if (original != null && pending != null) {
Gary Maia4adae12016-10-23 13:47:17 -0700945 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
Gary Mai698cee72016-09-19 16:09:54 -0700946 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
947 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
948
949 final String beforePrefix = original.getAsString(StructuredName.PREFIX);
950 final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
951 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
952
953 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
954 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
955 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
956
957 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
958 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
959 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
960
961 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
962 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
963 if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
964
965 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
966 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
967 return TextUtils.equals(beforeSuffix, afterSuffix);
968 }
969 return false;
970 }
971
Walter Jang7b0970f2016-09-01 10:40:19 -0700972 //
973 // Account creation
974 //
975
976 private void selectAccountAndCreateContact() {
977 // If this is a local profile, then skip the logic about showing the accounts changed
978 // activity and create a phone-local contact.
979 if (mNewLocalProfile) {
980 createContact(null);
981 return;
982 }
983
984 // If there is no default account or the accounts have changed such that we need to
985 // prompt the user again, then launch the account prompt.
986 if (mEditorUtils.shouldShowAccountChangedNotification()) {
987 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
988 // Prevent a second instance from being started on rotates
989 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
990 mStatus = Status.SUB_ACTIVITY;
991 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
992 } else {
Gary Mai3107b252016-11-02 18:26:07 -0700993 // Make sure the default account is automatically set if there is only one non-device
994 // account.
995 mEditorUtils.maybeUpdateDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -0700996 // Otherwise, there should be a default account. Then either create a local contact
997 // (if default account is null) or create a contact with the specified account.
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700998 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -0700999 createContact(defaultAccount);
1000 }
1001 }
1002
1003 /**
1004 * Create a contact by automatically selecting the first account. If there's no available
1005 * account, a device-local contact should be created.
1006 */
1007 private void createContact() {
1008 final List<AccountWithDataSet> accounts =
1009 AccountTypeManager.getInstance(mContext).getAccounts(true);
1010 // No Accounts available. Create a phone-local contact.
1011 if (accounts.isEmpty()) {
1012 createContact(null);
1013 return;
1014 }
1015
1016 // We have an account switcher in "create-account" screen, so don't need to ask a user to
1017 // select an account here.
1018 createContact(accounts.get(0));
1019 }
1020
1021 /**
1022 * Shows account creation screen associated with a given account.
1023 *
1024 * @param account may be null to signal a device-local contact should be created.
1025 */
1026 private void createContact(AccountWithDataSet account) {
1027 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1028 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1029
Gary Maiaebf3202016-09-22 18:11:15 -07001030 setStateForNewContact(account, accountType, isEditingUserProfile());
Walter Jang7b0970f2016-09-01 10:40:19 -07001031 }
1032
1033 //
1034 // Data binding
1035 //
1036
1037 private void setState(Contact contact) {
1038 // If we have already loaded data, we do not want to change it here to not confuse the user
1039 if (!mState.isEmpty()) {
1040 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1041 return;
1042 }
Gary Mai7b751452016-11-07 17:04:04 -08001043 mContact = contact;
Gary Mai4ceabed2016-09-16 12:14:13 -07001044 mRawContacts = contact.getRawContacts();
Walter Jang7b0970f2016-09-01 10:40:19 -07001045
Walter Jang7b0970f2016-09-01 10:40:19 -07001046 // Check for writable raw contacts. If there are none, then we need to create one so user
1047 // can edit. For the user profile case, there is already an editable contact.
1048 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1049 mHasNewContact = true;
Gary Mai698cee72016-09-19 16:09:54 -07001050 mReadOnlyDisplayNameId = contact.getNameRawContactId();
1051 mCopyReadOnlyName = true;
Walter Jang7b0970f2016-09-01 10:40:19 -07001052 // This is potentially an asynchronous call and will add deltas to list.
1053 selectAccountAndCreateContact();
Walter Jang7b0970f2016-09-01 10:40:19 -07001054 } else {
1055 mHasNewContact = false;
1056 }
1057
Gary Mai698cee72016-09-19 16:09:54 -07001058 setStateForExistingContact(contact.isUserProfile(), mRawContacts);
Gary Maie4874662016-09-26 11:42:54 -07001059 if (mAutoAddToDefaultGroup
1060 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1061 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1062 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001063 }
1064
1065 /**
1066 * Prepare {@link #mState} for a newly created phone-local contact.
1067 */
1068 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1069 boolean isUserProfile) {
1070 setStateForNewContact(account, accountType, /* oldState =*/ null,
1071 /* oldAccountType =*/ null, isUserProfile);
1072 }
1073
1074 /**
1075 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1076 * specified by oldState and oldAccountType.
1077 */
1078 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1079 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1080 mStatus = Status.EDITING;
1081 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1082 mIsUserProfile = isUserProfile;
1083 mNewContactDataReady = true;
1084 bindEditors();
1085 }
1086
1087 /**
1088 * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1089 * {@link #mState}.
1090 *
1091 * If oldState and oldAccountType are specified, the state specified by those parameters
1092 * is migrated to the result {@link RawContactDelta}.
1093 */
1094 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1095 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1096 final RawContact rawContact = new RawContact();
1097 if (account != null) {
1098 rawContact.setAccount(account);
1099 } else {
1100 rawContact.setAccountToLocal();
1101 }
1102
1103 final RawContactDelta result = new RawContactDelta(
1104 ValuesDelta.fromAfter(rawContact.getValues()));
1105 if (oldState == null) {
1106 // Parse any values from incoming intent
1107 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1108 } else {
1109 RawContactModifier.migrateStateForNewContact(
1110 mContext, oldState, result, oldAccountType, accountType);
1111 }
1112
1113 // Ensure we have some default fields (if the account type does not support a field,
1114 // ensureKind will not add it, so it is safe to add e.g. Event)
Gary Mai62ec0b12016-10-07 14:23:54 -07001115 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001116 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1117 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1118 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1119 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1120 RawContactModifier.ensureKindExists(result, accountType,
1121 StructuredPostal.CONTENT_ITEM_TYPE);
1122
1123 // Set the correct URI for saving the contact as a profile
1124 if (mNewLocalProfile) {
1125 result.setProfileQueryUri();
1126 }
1127
1128 return result;
1129 }
1130
1131 /**
1132 * Prepare {@link #mState} for an existing contact.
1133 */
Gary Mai698cee72016-09-19 16:09:54 -07001134 private void setStateForExistingContact(boolean isUserProfile,
Walter Jang7b0970f2016-09-01 10:40:19 -07001135 ImmutableList<RawContact> rawContacts) {
1136 setEnabled(true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001137
1138 mState.addAll(rawContacts.iterator());
1139 setIntentExtras(mIntentExtras);
1140 mIntentExtras = null;
1141
1142 // For user profile, change the contacts query URI
1143 mIsUserProfile = isUserProfile;
1144 boolean localProfileExists = false;
1145
1146 if (mIsUserProfile) {
1147 for (RawContactDelta rawContactDelta : mState) {
1148 // For profile contacts, we need a different query URI
1149 rawContactDelta.setProfileQueryUri();
1150 // Try to find a local profile contact
1151 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1152 localProfileExists = true;
1153 }
1154 }
1155 // Editor should always present a local profile for editing
1156 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
1157 // going to prune all but the one raw contact that we're trying to display by itself.
1158 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1159 mState.add(createLocalRawContactDelta());
1160 }
1161 }
1162 mExistingContactDataReady = true;
1163 bindEditors();
1164 }
1165
1166 /**
1167 * Set the enabled state of editors.
1168 */
1169 private void setEnabled(boolean enabled) {
1170 if (mEnabled != enabled) {
1171 mEnabled = enabled;
1172
1173 // Enable/disable editors
1174 if (mContent != null) {
1175 int count = mContent.getChildCount();
1176 for (int i = 0; i < count; i++) {
1177 mContent.getChildAt(i).setEnabled(enabled);
1178 }
1179 }
1180
1181 // Enable/disable aggregation suggestion vies
1182 if (mAggregationSuggestionView != null) {
1183 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
1184 R.id.aggregation_suggestions);
1185 int count = itemList.getChildCount();
1186 for (int i = 0; i < count; i++) {
1187 itemList.getChildAt(i).setEnabled(enabled);
1188 }
1189 }
1190
1191 // Maybe invalidate the options menu
1192 final Activity activity = getActivity();
1193 if (activity != null) activity.invalidateOptionsMenu();
1194 }
1195 }
1196
1197 /**
1198 * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1199 * {@link #mState}.
1200 */
1201 private static RawContactDelta createLocalRawContactDelta() {
1202 final RawContact rawContact = new RawContact();
1203 rawContact.setAccountToLocal();
1204
1205 final RawContactDelta result = new RawContactDelta(
1206 ValuesDelta.fromAfter(rawContact.getValues()));
1207 result.setProfileQueryUri();
1208
1209 return result;
1210 }
1211
Gary Mai698cee72016-09-19 16:09:54 -07001212 private void copyReadOnlyName() {
1213 // We should only ever be doing this if we're creating a new writable contact to attach to
1214 // a read only contact.
1215 if (!isEditingReadOnlyRawContactWithNewContact()) {
1216 return;
1217 }
1218 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1219 final RawContactDelta writable = mState.get(writableIndex);
Gary Mai7b751452016-11-07 17:04:04 -08001220 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
Gary Mai698cee72016-09-19 16:09:54 -07001221 final ValuesDelta writeNameDelta = writable
1222 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1223 final ValuesDelta readNameDelta = readOnly
1224 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1225 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
1226 mCopyReadOnlyName = false;
1227 }
1228
Walter Jang7b0970f2016-09-01 10:40:19 -07001229 /**
1230 * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1231 * Contact.
1232 */
Walter Jangba59deb2015-01-26 11:23:48 -08001233 protected void bindEditors() {
Walter Jangcab3dce2015-02-09 17:48:03 -08001234 if (!isReadyToBindEditors()) {
1235 return;
1236 }
1237
Walter Jangd35e5ef2015-02-24 09:18:16 -08001238 // Add input fields for the loaded Contact
Gary Mai363af602016-09-28 10:01:23 -07001239 final RawContactEditorView editorView = getContent();
Walter Jangb6ca2722015-02-20 11:10:25 -08001240 editorView.setListener(this);
Gary Mai698cee72016-09-19 16:09:54 -07001241 if (mCopyReadOnlyName) {
1242 copyReadOnlyName();
1243 }
Gary Mai678108e2016-10-26 14:34:33 -07001244 editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
Walter Jang9a552372016-08-24 11:51:05 -07001245 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
Gary Mai5a00de32016-10-19 18:20:41 -07001246 mRawContactIdToDisplayAlone);
Gary Mai079598f2016-11-03 15:02:45 -07001247 if (isEditingReadOnlyRawContact()) {
Gary Mai15646ce2016-11-17 10:54:01 -08001248 final Toolbar toolbar = getEditorActivity().getToolbar();
1249 if (toolbar != null) {
1250 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
Gary Maid8f3da62016-11-18 11:47:20 -08001251 // Set activity title for Talkback
1252 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
Gary Mai15646ce2016-11-17 10:54:01 -08001253 toolbar.setNavigationIcon(R.drawable.ic_back_arrow);
1254 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
Gary Mai079598f2016-11-03 15:02:45 -07001255 }
1256 }
Walter Jangcab3dce2015-02-09 17:48:03 -08001257
Walter Jangd35e5ef2015-02-24 09:18:16 -08001258 // Set up the photo widget
Walter Jang31a74ad2015-10-02 19:17:39 -07001259 editorView.setPhotoListener(this);
Walter Jang3efae4a2015-02-18 11:12:00 -08001260 mPhotoRawContactId = editorView.getPhotoRawContactId();
Walter Jang31a74ad2015-10-02 19:17:39 -07001261 // If there is an updated full resolution photo apply it now, this will be the case if
1262 // the user selects or takes a new photo, then rotates the device.
1263 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1264 if (uri != null) {
1265 editorView.setFullSizePhoto(uri);
Walter Jang41b3ea12015-03-09 17:30:06 -07001266 }
Walter Jang3efae4a2015-02-18 11:12:00 -08001267
Walter Jangd35e5ef2015-02-24 09:18:16 -08001268 // The editor is ready now so make it visible
Gary Mai678108e2016-10-26 14:34:33 -07001269 editorView.setEnabled(mEnabled);
Walter Jangd35e5ef2015-02-24 09:18:16 -08001270 editorView.setVisibility(View.VISIBLE);
1271
1272 // Refresh the ActionBar as the visibility of the join command
1273 // Activity can be null if we have been detached from the Activity.
Walter Jangcab3dce2015-02-09 17:48:03 -08001274 invalidateOptionsMenu();
1275 }
1276
Walter Jang7b0970f2016-09-01 10:40:19 -07001277 /**
1278 * Invalidates the options menu if we are still associated with an Activity.
1279 */
1280 private void invalidateOptionsMenu() {
1281 final Activity activity = getActivity();
1282 if (activity != null) {
1283 activity.invalidateOptionsMenu();
1284 }
1285 }
1286
Walter Jangcab3dce2015-02-09 17:48:03 -08001287 private boolean isReadyToBindEditors() {
1288 if (mState.isEmpty()) {
1289 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1290 Log.v(TAG, "No data to bind editors");
1291 }
1292 return false;
1293 }
1294 if (mIsEdit && !mExistingContactDataReady) {
1295 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1296 Log.v(TAG, "Existing contact data is not ready to bind editors.");
1297 }
1298 return false;
1299 }
1300 if (mHasNewContact && !mNewContactDataReady) {
1301 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1302 Log.v(TAG, "New contact data is not ready to bind editors.");
1303 }
1304 return false;
1305 }
1306 return true;
Walter Jangba59deb2015-01-26 11:23:48 -08001307 }
1308
Walter Jang7b0970f2016-09-01 10:40:19 -07001309 /**
1310 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1311 * Some of old data are reused with new restriction enforced by the new account.
1312 *
1313 * @param oldState Old data being edited.
1314 * @param oldAccount Old account associated with oldState.
1315 * @param newAccount New account to be used.
1316 */
1317 private void rebindEditorsForNewContact(
1318 RawContactDelta oldState, AccountWithDataSet oldAccount,
1319 AccountWithDataSet newAccount) {
1320 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1321 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1322 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1323
Gary Maiaebf3202016-09-22 18:11:15 -07001324 mExistingContactDataReady = false;
1325 mNewContactDataReady = false;
1326 mState = new RawContactDeltaList();
1327 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1328 isEditingUserProfile());
1329 if (mIsEdit) {
Gary Mai698cee72016-09-19 16:09:54 -07001330 setStateForExistingContact(isEditingUserProfile(), mRawContacts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001331 }
1332 }
1333
1334 //
1335 // ContactEditor
1336 //
1337
Walter Jang3f990ba2015-01-27 17:38:30 +00001338 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -07001339 public void setListener(Listener listener) {
1340 mListener = listener;
1341 }
1342
1343 @Override
1344 public void load(String action, Uri lookupUri, Bundle intentExtras) {
1345 mAction = action;
1346 mLookupUri = lookupUri;
1347 mIntentExtras = intentExtras;
1348
1349 if (mIntentExtras != null) {
1350 mAutoAddToDefaultGroup =
1351 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1352 mNewLocalProfile =
1353 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1354 mDisableDeleteMenuOption =
1355 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1356 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1357 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1358 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1359 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1360 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1361 }
Gary Maia6c80b32016-09-30 16:34:55 -07001362 mRawContactIdToDisplayAlone = mIntentExtras
1363 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001364 }
1365 }
1366
1367 @Override
1368 public void setIntentExtras(Bundle extras) {
Gary Mai5336e6e2016-10-23 14:17:03 -07001369 getContent().setIntentExtras(extras);
Walter Jang7b0970f2016-09-01 10:40:19 -07001370 }
1371
1372 @Override
1373 public void onJoinCompleted(Uri uri) {
1374 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1375 }
1376
James Laskeye5a140a2016-10-18 15:43:42 -07001377
1378 private String getNameToDisplay(Uri contactUri) {
Gary Maic000d2e2016-11-18 13:51:17 -08001379 // The contact has been deleted or the uri is otherwise no longer right.
1380 if (contactUri == null) {
1381 return null;
1382 }
James Laskeye5a140a2016-10-18 15:43:42 -07001383 final ContentResolver resolver = mContext.getContentResolver();
1384 final Cursor cursor = resolver.query(contactUri, new String[]{
1385 ContactsContract.Contacts.DISPLAY_NAME,
1386 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
James Laskeye5a140a2016-10-18 15:43:42 -07001387
Gary Maia4adae12016-10-23 13:47:17 -07001388 if (cursor != null) {
1389 try {
1390 if (cursor.moveToFirst()) {
1391 final String displayName = cursor.getString(0);
1392 final String displayNameAlt = cursor.getString(1);
1393 cursor.close();
1394 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1395 new ContactsPreferences(mContext));
1396 }
1397 } finally {
1398 cursor.close();
1399 }
1400 }
James Laskeye5a140a2016-10-18 15:43:42 -07001401 return null;
1402 }
1403
1404
Walter Jang7b0970f2016-09-01 10:40:19 -07001405 @Override
1406 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1407 Uri contactLookupUri, Long joinContactId) {
1408 if (hadChanges) {
1409 if (saveSucceeded) {
1410 switch (saveMode) {
1411 case SaveMode.JOIN:
1412 break;
1413 case SaveMode.SPLIT:
1414 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1415 .show();
1416 break;
1417 default:
James Laskeye5a140a2016-10-18 15:43:42 -07001418 final String displayName = getNameToDisplay(contactLookupUri);
James Laskeyb1671052016-09-16 13:57:21 -07001419 final String toastMessage;
1420 if (!TextUtils.isEmpty(displayName)) {
1421 toastMessage = getResources().getString(
1422 R.string.contactSavedNamedToast, displayName);
1423 } else {
1424 toastMessage = getResources().getString(R.string.contactSavedToast);
1425 }
1426 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
Walter Jang7b0970f2016-09-01 10:40:19 -07001427 }
1428
1429 } else {
1430 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1431 }
1432 }
1433 switch (saveMode) {
1434 case SaveMode.CLOSE: {
Walter Jang581585d2016-09-21 19:21:13 -07001435 Intent resultIntent = null;
Walter Jang7b0970f2016-09-01 10:40:19 -07001436 if (saveSucceeded && contactLookupUri != null) {
1437 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1438 mContext, contactLookupUri, mLookupUri);
Walter Jangdf86ede2016-10-19 09:48:29 -07001439 if (Flags.getInstance().getBoolean(Experiments.CONTACT_SHEET)) {
Walter Jang581585d2016-09-21 19:21:13 -07001440 resultIntent = ObjectFactory.getContactSheetIntent(mContext, lookupUri);
1441 }
1442 if (resultIntent == null) {
1443 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1444 mContext, lookupUri, ScreenType.EDITOR);
1445 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
1446 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001447 } else {
1448 resultIntent = null;
1449 }
1450 // It is already saved, so prevent it from being saved again
1451 mStatus = Status.CLOSING;
1452 if (mListener != null) mListener.onSaveFinished(resultIntent);
1453 break;
1454 }
Gary Mai363af602016-09-28 10:01:23 -07001455 case SaveMode.EDITOR: {
Walter Jang7b0970f2016-09-01 10:40:19 -07001456 // It is already saved, so prevent it from being saved again
1457 mStatus = Status.CLOSING;
1458 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1459 break;
1460 }
1461 case SaveMode.JOIN:
1462 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1463 joinAggregate(joinContactId);
1464 }
1465 break;
1466 case SaveMode.RELOAD:
1467 if (saveSucceeded && contactLookupUri != null) {
1468 // If this was in INSERT, we are changing into an EDIT now.
1469 // If it already was an EDIT, we are changing to the new Uri now
1470 mState = new RawContactDeltaList();
1471 load(Intent.ACTION_EDIT, contactLookupUri, null);
1472 mStatus = Status.LOADING;
1473 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1474 }
1475 break;
1476
1477 case SaveMode.SPLIT:
1478 mStatus = Status.CLOSING;
1479 if (mListener != null) {
1480 mListener.onContactSplit(contactLookupUri);
1481 } else {
1482 Log.d(TAG, "No listener registered, can not call onSplitFinished");
1483 }
1484 break;
1485 }
1486 }
1487
1488 /**
1489 * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1490 *
1491 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1492 */
1493 private void showJoinAggregateActivity(Uri contactLookupUri) {
1494 if (contactLookupUri == null || !isAdded()) {
1495 return;
1496 }
1497
1498 mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1499 final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1500 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1501 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1502 startActivityForResult(intent, REQUEST_CODE_JOIN);
1503 }
1504
1505 //
1506 // Aggregation PopupWindow
1507 //
1508
1509 /**
1510 * Triggers an asynchronous search for aggregation suggestions.
1511 */
1512 protected void acquireAggregationSuggestions(Context context,
1513 long rawContactId, ValuesDelta valuesDelta) {
1514 if (mAggregationSuggestionsRawContactId != rawContactId
1515 && mAggregationSuggestionView != null) {
1516 mAggregationSuggestionView.setVisibility(View.GONE);
1517 mAggregationSuggestionView = null;
1518 mAggregationSuggestionEngine.reset();
1519 }
1520
1521 mAggregationSuggestionsRawContactId = rawContactId;
1522
1523 if (mAggregationSuggestionEngine == null) {
1524 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1525 mAggregationSuggestionEngine.setListener(this);
1526 mAggregationSuggestionEngine.start();
1527 }
1528
1529 mAggregationSuggestionEngine.setContactId(getContactId());
Gary Mai220d10c2016-09-23 13:56:39 -07001530 mAggregationSuggestionEngine.setAccountFilter(
1531 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
Walter Jang7b0970f2016-09-01 10:40:19 -07001532
1533 mAggregationSuggestionEngine.onNameChange(valuesDelta);
1534 }
1535
1536 /**
1537 * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1538 */
1539 private long getContactId() {
1540 for (RawContactDelta rawContact : mState) {
1541 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1542 if (contactId != null) {
1543 return contactId;
1544 }
1545 }
1546 return 0;
1547 }
1548
1549 @Override
1550 public void onAggregationSuggestionChange() {
1551 final Activity activity = getActivity();
1552 if ((activity != null && activity.isFinishing())
1553 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
1554 return;
1555 }
1556
1557 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1558
1559 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1560 return;
1561 }
1562
Gary Maida20b472016-09-20 14:46:40 -07001563 final View anchorView = getAggregationAnchorView();
Walter Jang7b0970f2016-09-01 10:40:19 -07001564 if (anchorView == null) {
1565 return; // Raw contact deleted?
1566 }
1567 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1568 mAggregationSuggestionPopup.setAnchorView(anchorView);
1569 mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1570 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1571 mAggregationSuggestionPopup.setAdapter(
1572 new AggregationSuggestionAdapter(
1573 getActivity(),
Walter Jang7b0970f2016-09-01 10:40:19 -07001574 /* listener =*/ this,
1575 mAggregationSuggestionEngine.getSuggestions()));
1576 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1577 @Override
1578 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1579 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1580 suggestionView.handleItemClickEvent();
1581 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1582 mAggregationSuggestionPopup = null;
1583 }
1584 });
1585 mAggregationSuggestionPopup.show();
1586 }
1587
1588 /**
Gary Maida20b472016-09-20 14:46:40 -07001589 * Returns the editor view that should be used as the anchor for aggregation suggestions.
Walter Jang7b0970f2016-09-01 10:40:19 -07001590 */
Gary Maida20b472016-09-20 14:46:40 -07001591 protected View getAggregationAnchorView() {
Walter Jangd35e5ef2015-02-24 09:18:16 -08001592 return getContent().getAggregationAnchorView();
1593 }
1594
Walter Jang7b0970f2016-09-01 10:40:19 -07001595 /**
1596 * Joins the suggested contact (specified by the id's of constituent raw
1597 * contacts), save all changes, and stay in the editor.
1598 */
1599 public void doJoinSuggestedContact(long[] rawContactIds) {
1600 if (!hasValidState() || mStatus != Status.EDITING) {
1601 return;
1602 }
1603
1604 mState.setJoinWithRawContacts(rawContactIds);
1605 save(SaveMode.RELOAD);
1606 }
1607
1608 @Override
Gary Mai678108e2016-10-26 14:34:33 -07001609 public void onEditAction(Uri contactLookupUri, long rawContactId) {
1610 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
Walter Jang7b0970f2016-09-01 10:40:19 -07001611 }
1612
1613 /**
Gary Mai678108e2016-10-26 14:34:33 -07001614 * Abandons the currently edited contact and switches to editing the selected raw contact,
1615 * transferring all the data there
Walter Jang7b0970f2016-09-01 10:40:19 -07001616 */
Gary Mai678108e2016-10-26 14:34:33 -07001617 public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001618 if (mListener != null) {
1619 // make sure we don't save this contact when closing down
1620 mStatus = Status.CLOSING;
Gary Mai678108e2016-10-26 14:34:33 -07001621 mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1622 getContent().getCurrentRawContactDelta().getContentValues());
Walter Jang7b0970f2016-09-01 10:40:19 -07001623 }
1624 }
1625
1626 /**
1627 * Sets group metadata on all bound editors.
1628 */
Walter Jang92f8ccc2015-02-06 10:23:37 -08001629 protected void setGroupMetaData() {
Walter Jangf10ca152015-09-22 15:23:55 -07001630 if (mGroupMetaData != null) {
1631 getContent().setGroupMetaData(mGroupMetaData);
1632 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001633 }
1634
Walter Jang7b0970f2016-09-01 10:40:19 -07001635 /**
1636 * Persist the accumulated editor deltas.
1637 *
1638 * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1639 * may be null.
1640 */
Walter Jange3373dc2015-10-27 15:35:12 -07001641 protected boolean doSaveAction(int saveMode, Long joinContactId) {
Walter Jang49ed2032015-02-11 20:09:05 -08001642 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1643 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1644 ((Activity) mContext).getClass(),
Gary Mai363af602016-09-28 10:01:23 -07001645 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
Walter Jange3373dc2015-10-27 15:35:12 -07001646 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
Wenyi Wangdd7d4562015-12-08 13:33:43 -08001647 return startSaveService(mContext, intent, saveMode);
Walter Jang49ed2032015-02-11 20:09:05 -08001648 }
1649
Walter Jang7b0970f2016-09-01 10:40:19 -07001650 private boolean startSaveService(Context context, Intent intent, int saveMode) {
1651 final boolean result = ContactSaveService.startService(
1652 context, intent, saveMode);
1653 if (!result) {
1654 onCancelEditConfirmed();
1655 }
1656 return result;
1657 }
1658
1659 //
1660 // Join Activity
1661 //
1662
1663 /**
1664 * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1665 */
Walter Jang49ed2032015-02-11 20:09:05 -08001666 protected void joinAggregate(final long contactId) {
1667 final Intent intent = ContactSaveService.createJoinContactsIntent(
Gary Mai363af602016-09-28 10:01:23 -07001668 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1669 ContactEditorActivity.ACTION_JOIN_COMPLETED);
Walter Jang49ed2032015-02-11 20:09:05 -08001670 mContext.startService(intent);
Walter Jang3f990ba2015-01-27 17:38:30 +00001671 }
Walter Jangb6ca2722015-02-20 11:10:25 -08001672
Walter Jang31a74ad2015-10-02 19:17:39 -07001673 public void removePhoto() {
1674 getContent().removePhoto();
1675 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
Walter Jang0e72ce92015-02-23 12:27:21 -08001676 }
1677
Walter Jang31a74ad2015-10-02 19:17:39 -07001678 public void updatePhoto(Uri uri) throws FileNotFoundException {
1679 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1680 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Wenyi Wang9bc9ba82015-11-17 19:37:33 -08001681 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Walter Jang31a74ad2015-10-02 19:17:39 -07001682 Toast.LENGTH_SHORT).show();
1683 return;
Walter Jang0e72ce92015-02-23 12:27:21 -08001684 }
Walter Jang31a74ad2015-10-02 19:17:39 -07001685 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1686 getContent().updatePhoto(uri);
Walter Jang0e72ce92015-02-23 12:27:21 -08001687 }
1688
Gary Maida20b472016-09-20 14:46:40 -07001689 public void setPrimaryPhoto() {
1690 getContent().setPrimaryPhoto();
Walter Jang0e72ce92015-02-23 12:27:21 -08001691 }
1692
1693 @Override
Walter Jang151f3e62015-02-26 15:29:40 -08001694 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1695 final Activity activity = getActivity();
1696 if (activity == null || activity.isFinishing()) {
1697 return;
1698 }
Walter Jang45b86d52015-10-15 15:23:16 -07001699 acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
Walter Jang151f3e62015-02-26 15:29:40 -08001700 }
1701
Walter Jang5a7a23b2015-03-06 10:54:26 -08001702 @Override
Walter Jang708ea9e2015-09-10 15:42:05 -07001703 public void onRebindEditorsForNewContact(RawContactDelta oldState,
1704 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1705 mNewContactAccountChanged = true;
1706 mAccountWithDataSet = newAccount;
1707 rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1708 }
1709
Walter Jang79658e12015-09-24 10:36:26 -07001710 @Override
1711 public void onBindEditorsFailed() {
1712 final Activity activity = getActivity();
1713 if (activity != null && !activity.isFinishing()) {
Gary Mai363af602016-09-28 10:01:23 -07001714 Toast.makeText(activity, R.string.editor_failed_to_load,
Walter Jang79658e12015-09-24 10:36:26 -07001715 Toast.LENGTH_SHORT).show();
1716 activity.setResult(Activity.RESULT_CANCELED);
1717 activity.finish();
1718 }
1719 }
1720
Walter Jangd6753152015-10-02 09:23:13 -07001721 @Override
1722 public void onEditorsBound() {
Wenyi Wang3cb77bb2016-07-27 17:39:03 -07001723 final Activity activity = getActivity();
1724 if (activity == null || activity.isFinishing()) {
1725 return;
1726 }
Walter Jangd6753152015-10-02 09:23:13 -07001727 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1728 }
1729
Walter Jang31a74ad2015-10-02 19:17:39 -07001730 @Override
1731 public void onPhotoEditorViewClicked() {
Walter Jang3f18d612015-10-07 16:01:05 -07001732 // For contacts composed of a single writable raw contact, or raw contacts have no more
1733 // than 1 photo, clicking the photo view simply opens the source photo dialog
Walter Jang31a74ad2015-10-02 19:17:39 -07001734 getEditorActivity().changePhoto(getPhotoMode());
1735 }
1736
1737 private int getPhotoMode() {
Gary Maida20b472016-09-20 14:46:40 -07001738 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1739 : PhotoActionPopup.Modes.NO_PHOTO;
Walter Jang31a74ad2015-10-02 19:17:39 -07001740 }
1741
Gary Mai363af602016-09-28 10:01:23 -07001742 private ContactEditorActivity getEditorActivity() {
1743 return (ContactEditorActivity) getActivity();
Walter Jang31a74ad2015-10-02 19:17:39 -07001744 }
1745
Gary Mai363af602016-09-28 10:01:23 -07001746 private RawContactEditorView getContent() {
1747 return (RawContactEditorView) mContent;
Walter Jang3efae4a2015-02-18 11:12:00 -08001748 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001749}