blob: 213648f0081848d60e9c70cea594bac5336112e1 [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";
Walter Jang7b0970f2016-09-01 10:40:19 -0700133
134 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
164 private static final int CURRENT_API_VERSION = android.os.Build.VERSION.SDK_INT;
165
166 /**
167 * An intent extra that forces the editor to add the edited contact
168 * to the default group (e.g. "My Contacts").
169 */
170 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
171
172 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
173
174 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
175 "disableDeleteMenuOption";
176
177 /**
178 * Intent key to pass the photo palette primary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700179 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700180 */
181 public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
182 "material_palette_primary_color";
183
184 /**
185 * Intent key to pass the photo palette secondary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700186 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700187 */
188 public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
189 "material_palette_secondary_color";
190
191 /**
192 * Intent key to pass the ID of the photo to display on the editor.
193 */
Gary Maida20b472016-09-20 14:46:40 -0700194 // TODO: This can be cleaned up if we decide to not pass the photo id through
195 // QuickContactActivity.
Walter Jang7b0970f2016-09-01 10:40:19 -0700196 public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
197
198 /**
Gary Maia6c80b32016-09-30 16:34:55 -0700199 * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
200 * by itself.
201 */
202 public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
203 "raw_contact_id_to_display_alone";
204
205 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700206 * Intent extra to specify a {@link ContactEditor.SaveMode}.
207 */
208 public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
209
210 /**
211 * Intent extra key for the contact ID to join the current contact to after saving.
212 */
213 public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
214
215 /**
216 * Callbacks for Activities that host contact editors Fragments.
217 */
218 public interface Listener {
219
220 /**
221 * Contact was not found, so somehow close this fragment. This is raised after a contact
222 * is removed via Menu/Delete
223 */
224 void onContactNotFound();
225
226 /**
227 * Contact was split, so we can close now.
228 *
229 * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
230 * The editor tries best to chose the most natural contact here.
231 */
232 void onContactSplit(Uri newLookupUri);
233
234 /**
235 * User has tapped Revert, close the fragment now.
236 */
237 void onReverted();
238
239 /**
240 * Contact was saved and the Fragment can now be closed safely.
241 */
242 void onSaveFinished(Intent resultIntent);
243
244 /**
Gary Mai678108e2016-10-26 14:34:33 -0700245 * User switched to editing a different raw contact (a suggestion from the
Walter Jang7b0970f2016-09-01 10:40:19 -0700246 * aggregation engine).
247 */
Gary Mai678108e2016-10-26 14:34:33 -0700248 void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
Walter Jang7b0970f2016-09-01 10:40:19 -0700249 ArrayList<ContentValues> contentValues);
250
251 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700252 * User has requested that contact be deleted.
253 */
254 void onDeleteRequested(Uri contactUri);
255 }
256
257 /**
258 * Adapter for aggregation suggestions displayed in a PopupWindow when
259 * editor fields change.
260 */
261 private static final class AggregationSuggestionAdapter extends BaseAdapter {
262 private final LayoutInflater mLayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -0700263 private final AggregationSuggestionView.Listener mListener;
264 private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
265
Gary Mai678108e2016-10-26 14:34:33 -0700266 public AggregationSuggestionAdapter(Activity activity,
Walter Jang7b0970f2016-09-01 10:40:19 -0700267 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
268 mLayoutInflater = activity.getLayoutInflater();
Walter Jang7b0970f2016-09-01 10:40:19 -0700269 mListener = listener;
270 mSuggestions = suggestions;
271 }
272
273 @Override
274 public View getView(int position, View convertView, ViewGroup parent) {
275 final Suggestion suggestion = (Suggestion) getItem(position);
276 final AggregationSuggestionView suggestionView =
277 (AggregationSuggestionView) mLayoutInflater.inflate(
278 R.layout.aggregation_suggestions_item, null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700279 suggestionView.setListener(mListener);
280 suggestionView.bindSuggestion(suggestion);
281 return suggestionView;
282 }
283
284 @Override
285 public long getItemId(int position) {
286 return position;
287 }
288
289 @Override
290 public Object getItem(int position) {
291 return mSuggestions.get(position);
292 }
293
294 @Override
295 public int getCount() {
296 return mSuggestions.size();
297 }
298 }
299
300 protected Context mContext;
301 protected Listener mListener;
302
303 //
304 // Views
305 //
306 protected LinearLayout mContent;
Walter Jang7b0970f2016-09-01 10:40:19 -0700307 protected ListPopupWindow mAggregationSuggestionPopup;
308
309 //
310 // Parameters passed in on {@link #load}
311 //
312 protected String mAction;
313 protected Uri mLookupUri;
314 protected Bundle mIntentExtras;
315 protected boolean mAutoAddToDefaultGroup;
316 protected boolean mDisableDeleteMenuOption;
317 protected boolean mNewLocalProfile;
318 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang7b0970f2016-09-01 10:40:19 -0700319
320 //
321 // Helpers
322 //
323 protected ContactEditorUtils mEditorUtils;
324 protected RawContactDeltaComparator mComparator;
325 protected ViewIdGenerator mViewIdGenerator;
326 private AggregationSuggestionEngine mAggregationSuggestionEngine;
327
328 //
329 // Loaded data
330 //
331 // Used to store existing contact data so it can be re-applied during a rebind call,
332 // i.e. account switch.
Gary Mai7b751452016-11-07 17:04:04 -0800333 protected Contact mContact;
Walter Jang7b0970f2016-09-01 10:40:19 -0700334 protected ImmutableList<RawContact> mRawContacts;
335 protected Cursor mGroupMetaData;
336
337 //
338 // Editor state
339 //
340 protected RawContactDeltaList mState;
341 protected int mStatus;
342 protected long mRawContactIdToDisplayAlone = -1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700343
344 // Whether to show the new contact blank form and if it's corresponding delta is ready.
345 protected boolean mHasNewContact;
346 protected AccountWithDataSet mAccountWithDataSet;
347 protected boolean mNewContactDataReady;
348 protected boolean mNewContactAccountChanged;
349
350 // Whether it's an edit of existing contact and if it's corresponding delta is ready.
351 protected boolean mIsEdit;
352 protected boolean mExistingContactDataReady;
353
354 // Whether we are editing the "me" profile
355 protected boolean mIsUserProfile;
356
Walter Jang7b0970f2016-09-01 10:40:19 -0700357 // Whether editor views and options menu items should be enabled
358 private boolean mEnabled = true;
359
360 // Aggregation PopupWindow
361 private long mAggregationSuggestionsRawContactId;
362
363 // Join Activity
364 protected long mContactIdForJoin;
365
366 // 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 -0700367 protected long mReadOnlyDisplayNameId;
368 protected boolean mCopyReadOnlyName;
Walter Jang7b0970f2016-09-01 10:40:19 -0700369
370 /**
371 * The contact data loader listener.
372 */
373 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
374 new LoaderManager.LoaderCallbacks<Contact>() {
375
376 protected long mLoaderStartTime;
377
378 @Override
379 public Loader<Contact> onCreateLoader(int id, Bundle args) {
380 mLoaderStartTime = SystemClock.elapsedRealtime();
Gary Maie4874662016-09-26 11:42:54 -0700381 return new ContactLoader(mContext, mLookupUri,
382 /* postViewNotification */ true,
383 /* loadGroupMetaData */ true);
Walter Jang7b0970f2016-09-01 10:40:19 -0700384 }
385
386 @Override
387 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
388 final long loaderCurrentTime = SystemClock.elapsedRealtime();
389 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
390 if (!contact.isLoaded()) {
391 // Item has been deleted. Close activity without saving again.
392 Log.i(TAG, "No contact found. Closing activity");
393 mStatus = Status.CLOSING;
394 if (mListener != null) mListener.onContactNotFound();
395 return;
396 }
397
398 mStatus = Status.EDITING;
399 mLookupUri = contact.getLookupUri();
400 final long setDataStartTime = SystemClock.elapsedRealtime();
401 setState(contact);
Walter Jang7b0970f2016-09-01 10:40:19 -0700402 final long setDataEndTime = SystemClock.elapsedRealtime();
403
404 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
405 }
406
407 @Override
408 public void onLoaderReset(Loader<Contact> loader) {
409 }
410 };
411
412 /**
413 * The groups meta data loader listener.
414 */
415 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
416 new LoaderManager.LoaderCallbacks<Cursor>() {
417
418 @Override
419 public CursorLoader onCreateLoader(int id, Bundle args) {
Gary Mai5c1bff22016-09-30 15:10:25 -0700420 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
421 GroupUtil.ALL_GROUPS_SELECTION);
Walter Jang7b0970f2016-09-01 10:40:19 -0700422 }
423
424 @Override
425 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
426 mGroupMetaData = data;
427 setGroupMetaData();
428 }
429
430 @Override
431 public void onLoaderReset(Loader<Cursor> loader) {
432 }
433 };
434
Walter Jang3efae4a2015-02-18 11:12:00 -0800435 private long mPhotoRawContactId;
Walter Jang28a27272015-09-19 16:06:08 -0700436 private Bundle mUpdatedPhotos = new Bundle();
Walter Jang3efae4a2015-02-18 11:12:00 -0800437
438 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700439 public Context getContext() {
440 return getActivity();
441 }
442
443 @Override
444 public void onAttach(Activity activity) {
445 super.onAttach(activity);
446 mContext = activity;
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700447 mEditorUtils = ContactEditorUtils.create(mContext);
Walter Jang7b0970f2016-09-01 10:40:19 -0700448 mComparator = new RawContactDeltaComparator(mContext);
449 }
450
451 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800452 public void onCreate(Bundle savedState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700453 if (savedState != null) {
454 // Restore mUri before calling super.onCreate so that onInitializeLoaders
455 // would already have a uri and an action to work with
456 mAction = savedState.getString(KEY_ACTION);
457 mLookupUri = savedState.getParcelable(KEY_URI);
458 }
459
Walter Jang3efae4a2015-02-18 11:12:00 -0800460 super.onCreate(savedState);
461
Walter Jang7b0970f2016-09-01 10:40:19 -0700462 if (savedState == null) {
463 mViewIdGenerator = new ViewIdGenerator();
464
465 // mState can still be null because it may not have have finished loading before
466 // onSaveInstanceState was called.
467 mState = new RawContactDeltaList();
468 } else {
469 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
470
471 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
472 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
473 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
474 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
Walter Jang7b0970f2016-09-01 10:40:19 -0700475
476 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
477 KEY_RAW_CONTACTS));
478 // NOTE: mGroupMetaData is not saved/restored
479
480 // Read state from savedState. No loading involved here
481 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
482 mStatus = savedState.getInt(KEY_STATUS);
Walter Jang7b0970f2016-09-01 10:40:19 -0700483
484 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
485 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
486
487 mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
488 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
489
490 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
491
Walter Jang7b0970f2016-09-01 10:40:19 -0700492 mEnabled = savedState.getBoolean(KEY_ENABLED);
493
494 // Aggregation PopupWindow
495 mAggregationSuggestionsRawContactId = savedState.getLong(
496 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
497
498 // Join Activity
499 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
500
Gary Mai698cee72016-09-19 16:09:54 -0700501 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
502 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700503
Walter Jang3efae4a2015-02-18 11:12:00 -0800504 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
Walter Jang28a27272015-09-19 16:06:08 -0700505 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
Walter Jang3efae4a2015-02-18 11:12:00 -0800506 }
507 }
508
Walter Jang3f990ba2015-01-27 17:38:30 +0000509 @Override
Walter Jang3f990ba2015-01-27 17:38:30 +0000510 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Walter Janged8f6c92015-01-30 16:07:47 -0800511 setHasOptionsMenu(true);
512
Walter Jang3f990ba2015-01-27 17:38:30 +0000513 final View view = inflater.inflate(
Gary Mai363af602016-09-28 10:01:23 -0700514 R.layout.contact_editor_fragment, container, false);
Walter Jangf5dfea42015-09-16 12:30:36 -0700515 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
Walter Jang3f990ba2015-01-27 17:38:30 +0000516 return view;
517 }
518
Walter Janged8f6c92015-01-30 16:07:47 -0800519 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700520 public void onActivityCreated(Bundle savedInstanceState) {
521 super.onActivityCreated(savedInstanceState);
522
523 validateAction(mAction);
524
525 if (mState.isEmpty()) {
526 // The delta list may not have finished loading before orientation change happens.
527 // In this case, there will be a saved state but deltas will be missing. Reload from
528 // database.
529 if (Intent.ACTION_EDIT.equals(mAction)) {
530 // Either
531 // 1) orientation change but load never finished.
532 // 2) not an orientation change so data needs to be loaded for first time.
533 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
534 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
535 }
536 } else {
537 // Orientation change, we already have mState, it was loaded by onCreate
538 bindEditors();
539 }
540
541 // Handle initial actions only when existing state missing
542 if (savedInstanceState == null) {
543 final Account account = mIntentExtras == null ? null :
544 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
545 final String dataSet = mIntentExtras == null ? null :
546 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
547 if (account != null) {
548 mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
549 }
550
551 if (Intent.ACTION_EDIT.equals(mAction)) {
552 mIsEdit = true;
553 } else if (Intent.ACTION_INSERT.equals(mAction)) {
554 mHasNewContact = true;
555 if (mAccountWithDataSet != null) {
556 createContact(mAccountWithDataSet);
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700557 } else if (mIntentExtras != null && mIntentExtras.getBoolean(
Gary Mai363af602016-09-28 10:01:23 -0700558 ContactEditorActivity.EXTRA_SAVE_TO_DEVICE_FLAG, false)) {
Marcus Hagerott935b56a2016-09-07 11:59:35 -0700559 createContact(null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700560 } else {
561 // No Account specified. Let the user choose
562 // Load Accounts async so that we can present them
563 selectAccountAndCreateContact();
564 }
565 }
566 }
567 }
568
569 /**
570 * Checks if the requested action is valid.
571 *
572 * @param action The action to test.
573 * @throws IllegalArgumentException when the action is invalid.
574 */
575 private static void validateAction(String action) {
576 if (VALID_INTENT_ACTIONS.contains(action)) {
577 return;
578 }
579 throw new IllegalArgumentException(
580 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
581 }
582
583 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800584 public void onSaveInstanceState(Bundle outState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700585 outState.putString(KEY_ACTION, mAction);
586 outState.putParcelable(KEY_URI, mLookupUri);
587 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
588 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
589 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
590 if (mMaterialPalette != null) {
591 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
592 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700593 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
594
595 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
596 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
597 // NOTE: mGroupMetaData is not saved
598
Gary Mai36ceb422016-10-17 14:04:17 -0700599 outState.putParcelable(KEY_EDIT_STATE, mState);
Walter Jang7b0970f2016-09-01 10:40:19 -0700600 outState.putInt(KEY_STATUS, mStatus);
601 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
602 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
603 outState.putBoolean(KEY_IS_EDIT, mIsEdit);
604 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
Walter Jang7b0970f2016-09-01 10:40:19 -0700605
606 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
607
Walter Jang7b0970f2016-09-01 10:40:19 -0700608 outState.putBoolean(KEY_ENABLED, mEnabled);
609
610 // Aggregation PopupWindow
611 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
612 mAggregationSuggestionsRawContactId);
613
614 // Join Activity
615 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
616
Gary Mai698cee72016-09-19 16:09:54 -0700617 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
618 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
Walter Jang7b0970f2016-09-01 10:40:19 -0700619
Walter Jang3efae4a2015-02-18 11:12:00 -0800620 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
Walter Jang28a27272015-09-19 16:06:08 -0700621 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
Walter Jang3efae4a2015-02-18 11:12:00 -0800622 super.onSaveInstanceState(outState);
623 }
624
625 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700626 public void onStop() {
627 super.onStop();
628 UiClosables.closeQuietly(mAggregationSuggestionPopup);
629 }
630
631 @Override
632 public void onDestroy() {
633 super.onDestroy();
634 if (mAggregationSuggestionEngine != null) {
635 mAggregationSuggestionEngine.quit();
636 }
637 }
638
639 @Override
640 public void onActivityResult(int requestCode, int resultCode, Intent data) {
641 switch (requestCode) {
642 case REQUEST_CODE_JOIN: {
643 // Ignore failed requests
644 if (resultCode != Activity.RESULT_OK) return;
645 if (data != null) {
646 final long contactId = ContentUris.parseId(data.getData());
647 if (hasPendingChanges()) {
648 // Ask the user if they want to save changes before doing the join
649 JoinContactConfirmationDialogFragment.show(this, contactId);
650 } else {
651 // Do the join immediately
652 joinAggregate(contactId);
653 }
654 }
655 break;
656 }
657 case REQUEST_CODE_ACCOUNTS_CHANGED: {
658 // Bail if the account selector was not successful.
659 if (resultCode != Activity.RESULT_OK) {
660 if (mListener != null) {
661 mListener.onReverted();
662 }
663 return;
664 }
665 // If there's an account specified, use it.
666 if (data != null) {
667 AccountWithDataSet account = data.getParcelableExtra(
668 Intents.Insert.EXTRA_ACCOUNT);
669 if (account != null) {
670 createContact(account);
671 return;
672 }
673 }
674 // If there isn't an account specified, then this is likely a phone-local
675 // contact, so we should continue setting up the editor by automatically selecting
676 // the most appropriate account.
677 createContact();
678 break;
679 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700680 }
681 }
682
Walter Jang7b0970f2016-09-01 10:40:19 -0700683 //
684 // Options menu
685 //
686
Walter Jang7b0970f2016-09-01 10:40:19 -0700687 @Override
688 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
689 inflater.inflate(R.menu.edit_contact, menu);
690 }
691
692 @Override
693 public void onPrepareOptionsMenu(Menu menu) {
694 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
695 // because the custom action bar contains the "save" button now (not the overflow menu).
696 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
697 final MenuItem saveMenu = menu.findItem(R.id.menu_save);
698 final MenuItem splitMenu = menu.findItem(R.id.menu_split);
699 final MenuItem joinMenu = menu.findItem(R.id.menu_join);
Walter Jang7b0970f2016-09-01 10:40:19 -0700700 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
701
Gary Mai5eda2572016-10-11 18:01:32 -0700702 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
703 // on a raw contact level.
704 joinMenu.setVisible(false);
705 splitMenu.setVisible(false);
706 deleteMenu.setVisible(false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700707 // Save menu is invisible when there's only one read only contact in the editor.
Gary Maid7faa652016-10-03 11:53:39 -0700708 saveMenu.setVisible(!isEditingReadOnlyRawContact());
Walter Jang7b0970f2016-09-01 10:40:19 -0700709 if (saveMenu.isVisible()) {
710 // Since we're using a custom action layout we have to manually hook up the handler.
711 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
712 @Override
713 public void onClick(View v) {
714 onOptionsItemSelected(saveMenu);
715 }
716 });
717 }
718
Walter Jang7b0970f2016-09-01 10:40:19 -0700719 int size = menu.size();
720 for (int i = 0; i < size; i++) {
721 menu.getItem(i).setEnabled(mEnabled);
722 }
723 }
724
725 @Override
Walter Jangc90cc152015-06-19 14:15:08 -0700726 public boolean onOptionsItemSelected(MenuItem item) {
727 if (item.getItemId() == android.R.id.home) {
728 return revert();
729 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700730
731 final Activity activity = getActivity();
732 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
733 // If we no longer are attached to a running activity want to
734 // drain this event.
735 return true;
736 }
737
738 switch (item.getItemId()) {
739 case R.id.menu_save:
740 return save(SaveMode.CLOSE);
741 case R.id.menu_delete:
742 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
743 return true;
744 case R.id.menu_split:
745 return doSplitContactAction();
746 case R.id.menu_join:
747 return doJoinContactAction();
Gary Maia4adae12016-10-23 13:47:17 -0700748 case R.id.menu_help:
749 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
Walter Jang7b0970f2016-09-01 10:40:19 -0700750 return true;
751 }
752
753 return false;
Walter Jangc90cc152015-06-19 14:15:08 -0700754 }
755
756 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700757 public boolean revert() {
758 if (mState.isEmpty() || !hasPendingChanges()) {
759 onCancelEditConfirmed();
760 } else {
761 CancelEditDialogFragment.show(this);
762 }
763 return true;
764 }
765
766 @Override
767 public void onCancelEditConfirmed() {
768 // When this Fragment is closed we don't want it to auto-save
769 mStatus = Status.CLOSING;
770 if (mListener != null) {
771 mListener.onReverted();
772 }
773 }
774
775 @Override
776 public void onSplitContactConfirmed(boolean hasPendingChanges) {
777 if (mState.isEmpty()) {
778 // This may happen when this Fragment is recreated by the system during users
779 // confirming the split action (and thus this method is called just before onCreate()),
780 // for example.
781 Log.e(TAG, "mState became null during the user's confirming split action. " +
782 "Cannot perform the save action.");
783 return;
784 }
785
786 if (!hasPendingChanges && mHasNewContact) {
787 // If the user didn't add anything new, we don't want to split out the newly created
788 // raw contact into a name-only contact so remove them.
789 final Iterator<RawContactDelta> iterator = mState.iterator();
790 while (iterator.hasNext()) {
791 final RawContactDelta rawContactDelta = iterator.next();
792 if (rawContactDelta.getRawContactId() < 0) {
793 iterator.remove();
794 }
795 }
796 }
797 mState.markRawContactsForSplitting();
798 save(SaveMode.SPLIT);
799 }
800
Gary Maib9065dd2016-11-08 10:49:00 -0800801 @Override
802 public void onSplitContactCanceled() {}
803
Walter Jang7b0970f2016-09-01 10:40:19 -0700804 private boolean doSplitContactAction() {
805 if (!hasValidState()) return false;
806
807 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
808 return true;
809 }
810
811 private boolean doJoinContactAction() {
812 if (!hasValidState() || mLookupUri == null) {
813 return false;
814 }
815
816 // If we just started creating a new contact and haven't added any data, it's too
817 // early to do a join
818 if (mState.size() == 1 && mState.get(0).isContactInsert()
819 && !hasPendingChanges()) {
820 Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
821 Toast.LENGTH_LONG).show();
822 return true;
823 }
824
825 showJoinAggregateActivity(mLookupUri);
826 return true;
827 }
828
829 @Override
830 public void onJoinContactConfirmed(long joinContactId) {
831 doSaveAction(SaveMode.JOIN, joinContactId);
832 }
833
Walter Jang7b0970f2016-09-01 10:40:19 -0700834 @Override
835 public boolean save(int saveMode) {
836 if (!hasValidState() || mStatus != Status.EDITING) {
837 return false;
838 }
839
840 // If we are about to close the editor - there is no need to refresh the data
Gary Mai363af602016-09-28 10:01:23 -0700841 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
Walter Jang7b0970f2016-09-01 10:40:19 -0700842 || saveMode == SaveMode.SPLIT) {
843 getLoaderManager().destroyLoader(LOADER_CONTACT);
844 }
845
846 mStatus = Status.SAVING;
847
848 if (!hasPendingChanges()) {
849 if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
850 // We don't have anything to save and there isn't even an existing contact yet.
851 // Nothing to do, simply go back to editing mode
852 mStatus = Status.EDITING;
853 return true;
854 }
855 onSaveCompleted(/* hadChanges =*/ false, saveMode,
856 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
857 return true;
858 }
859
860 setEnabled(false);
861
862 return doSaveAction(saveMode, /* joinContactId */ null);
863 }
864
865 //
866 // State accessor methods
867 //
868
869 /**
870 * Check if our internal {@link #mState} is valid, usually checked before
871 * performing user actions.
872 */
873 private boolean hasValidState() {
874 return mState.size() > 0;
875 }
876
877 private boolean isEditingUserProfile() {
878 return mNewLocalProfile || mIsUserProfile;
879 }
880
881 /**
Gary Mai5a00de32016-10-19 18:20:41 -0700882 * Whether the contact being edited is composed of read-only raw contacts
Walter Jang7b0970f2016-09-01 10:40:19 -0700883 * aggregated with a newly created writable raw contact.
884 */
885 private boolean isEditingReadOnlyRawContactWithNewContact() {
Gary Mai5a00de32016-10-19 18:20:41 -0700886 return mHasNewContact && mState.size() > 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700887 }
888
889 /**
Gary Maid7faa652016-10-03 11:53:39 -0700890 * @return true if the single raw contact we're looking at is read-only.
891 */
892 private boolean isEditingReadOnlyRawContact() {
893 return hasValidState() && mRawContactIdToDisplayAlone > 0
894 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
895 .getAccountType(AccountTypeManager.getInstance(mContext))
896 .areContactsWritable();
897 }
898
899 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700900 * Return true if there are any edits to the current contact which need to
901 * be saved.
902 */
903 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
904 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
905 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
906 }
907
908 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700909 * Determines if changes were made in the editor that need to be saved, while taking into
910 * account that name changes are not real for read-only contacts.
911 * See go/editing-read-only-contacts
912 */
913 private boolean hasPendingChanges() {
Gary Mai698cee72016-09-19 16:09:54 -0700914 if (isEditingReadOnlyRawContactWithNewContact()) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700915 // We created a new raw contact delta with a default display name.
916 // We must test for pending changes while ignoring the default display name.
Gary Mai698cee72016-09-19 16:09:54 -0700917 final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId)
918 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
919 final ValuesDelta pendingDelta = mState
920 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
921 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700922 final Set<String> excludedMimeTypes = new HashSet<>();
923 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
924 return hasPendingRawContactChanges(excludedMimeTypes);
925 }
926 return true;
927 }
928 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
929 }
930
931 /**
Gary Mai698cee72016-09-19 16:09:54 -0700932 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
933 * of a read only delta and now we want to check if the copied delta has changes.
934 *
935 * @param before original {@link ValuesDelta}
936 * @param after copied {@link ValuesDelta}
937 * @return true if the copied {@link ValuesDelta} has all the same values in the structured
938 * name fields as the original.
939 */
940 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
Gary Mai5a00de32016-10-19 18:20:41 -0700941 if (before == after) return true;
Gary Mai698cee72016-09-19 16:09:54 -0700942 if (before == null || after == null) return false;
943 final ContentValues original = before.getBefore();
944 final ContentValues pending = after.getAfter();
945 if (original != null && pending != null) {
Gary Maia4adae12016-10-23 13:47:17 -0700946 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
Gary Mai698cee72016-09-19 16:09:54 -0700947 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
948 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
949
950 final String beforePrefix = original.getAsString(StructuredName.PREFIX);
951 final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
952 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
953
954 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
955 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
956 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
957
958 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
959 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
960 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
961
962 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
963 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
964 if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
965
966 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
967 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
968 return TextUtils.equals(beforeSuffix, afterSuffix);
969 }
970 return false;
971 }
972
Walter Jang7b0970f2016-09-01 10:40:19 -0700973 //
974 // Account creation
975 //
976
977 private void selectAccountAndCreateContact() {
978 // If this is a local profile, then skip the logic about showing the accounts changed
979 // activity and create a phone-local contact.
980 if (mNewLocalProfile) {
981 createContact(null);
982 return;
983 }
984
985 // If there is no default account or the accounts have changed such that we need to
986 // prompt the user again, then launch the account prompt.
987 if (mEditorUtils.shouldShowAccountChangedNotification()) {
988 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
989 // Prevent a second instance from being started on rotates
990 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
991 mStatus = Status.SUB_ACTIVITY;
992 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
993 } else {
Gary Mai3107b252016-11-02 18:26:07 -0700994 // Make sure the default account is automatically set if there is only one non-device
995 // account.
996 mEditorUtils.maybeUpdateDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -0700997 // Otherwise, there should be a default account. Then either create a local contact
998 // (if default account is null) or create a contact with the specified account.
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700999 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount();
Walter Jang7b0970f2016-09-01 10:40:19 -07001000 createContact(defaultAccount);
1001 }
1002 }
1003
1004 /**
1005 * Create a contact by automatically selecting the first account. If there's no available
1006 * account, a device-local contact should be created.
1007 */
1008 private void createContact() {
1009 final List<AccountWithDataSet> accounts =
1010 AccountTypeManager.getInstance(mContext).getAccounts(true);
1011 // No Accounts available. Create a phone-local contact.
1012 if (accounts.isEmpty()) {
1013 createContact(null);
1014 return;
1015 }
1016
1017 // We have an account switcher in "create-account" screen, so don't need to ask a user to
1018 // select an account here.
1019 createContact(accounts.get(0));
1020 }
1021
1022 /**
1023 * Shows account creation screen associated with a given account.
1024 *
1025 * @param account may be null to signal a device-local contact should be created.
1026 */
1027 private void createContact(AccountWithDataSet account) {
1028 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1029 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1030
Gary Maiaebf3202016-09-22 18:11:15 -07001031 setStateForNewContact(account, accountType, isEditingUserProfile());
Walter Jang7b0970f2016-09-01 10:40:19 -07001032 }
1033
1034 //
1035 // Data binding
1036 //
1037
1038 private void setState(Contact contact) {
1039 // If we have already loaded data, we do not want to change it here to not confuse the user
1040 if (!mState.isEmpty()) {
1041 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1042 return;
1043 }
Gary Mai7b751452016-11-07 17:04:04 -08001044 mContact = contact;
Gary Mai4ceabed2016-09-16 12:14:13 -07001045 mRawContacts = contact.getRawContacts();
Walter Jang7b0970f2016-09-01 10:40:19 -07001046
Walter Jang7b0970f2016-09-01 10:40:19 -07001047 // Check for writable raw contacts. If there are none, then we need to create one so user
1048 // can edit. For the user profile case, there is already an editable contact.
1049 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1050 mHasNewContact = true;
Gary Mai698cee72016-09-19 16:09:54 -07001051 mReadOnlyDisplayNameId = contact.getNameRawContactId();
1052 mCopyReadOnlyName = true;
Walter Jang7b0970f2016-09-01 10:40:19 -07001053 // This is potentially an asynchronous call and will add deltas to list.
1054 selectAccountAndCreateContact();
Walter Jang7b0970f2016-09-01 10:40:19 -07001055 } else {
1056 mHasNewContact = false;
1057 }
1058
Gary Mai698cee72016-09-19 16:09:54 -07001059 setStateForExistingContact(contact.isUserProfile(), mRawContacts);
Gary Maie4874662016-09-26 11:42:54 -07001060 if (mAutoAddToDefaultGroup
1061 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1062 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1063 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001064 }
1065
1066 /**
1067 * Prepare {@link #mState} for a newly created phone-local contact.
1068 */
1069 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1070 boolean isUserProfile) {
1071 setStateForNewContact(account, accountType, /* oldState =*/ null,
1072 /* oldAccountType =*/ null, isUserProfile);
1073 }
1074
1075 /**
1076 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1077 * specified by oldState and oldAccountType.
1078 */
1079 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1080 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1081 mStatus = Status.EDITING;
1082 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1083 mIsUserProfile = isUserProfile;
1084 mNewContactDataReady = true;
1085 bindEditors();
1086 }
1087
1088 /**
1089 * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1090 * {@link #mState}.
1091 *
1092 * If oldState and oldAccountType are specified, the state specified by those parameters
1093 * is migrated to the result {@link RawContactDelta}.
1094 */
1095 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1096 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1097 final RawContact rawContact = new RawContact();
1098 if (account != null) {
1099 rawContact.setAccount(account);
1100 } else {
1101 rawContact.setAccountToLocal();
1102 }
1103
1104 final RawContactDelta result = new RawContactDelta(
1105 ValuesDelta.fromAfter(rawContact.getValues()));
1106 if (oldState == null) {
1107 // Parse any values from incoming intent
1108 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1109 } else {
1110 RawContactModifier.migrateStateForNewContact(
1111 mContext, oldState, result, oldAccountType, accountType);
1112 }
1113
1114 // Ensure we have some default fields (if the account type does not support a field,
1115 // ensureKind will not add it, so it is safe to add e.g. Event)
Gary Mai62ec0b12016-10-07 14:23:54 -07001116 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001117 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1118 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1119 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1120 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1121 RawContactModifier.ensureKindExists(result, accountType,
1122 StructuredPostal.CONTENT_ITEM_TYPE);
1123
1124 // Set the correct URI for saving the contact as a profile
1125 if (mNewLocalProfile) {
1126 result.setProfileQueryUri();
1127 }
1128
1129 return result;
1130 }
1131
1132 /**
1133 * Prepare {@link #mState} for an existing contact.
1134 */
Gary Mai698cee72016-09-19 16:09:54 -07001135 private void setStateForExistingContact(boolean isUserProfile,
Walter Jang7b0970f2016-09-01 10:40:19 -07001136 ImmutableList<RawContact> rawContacts) {
1137 setEnabled(true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001138
1139 mState.addAll(rawContacts.iterator());
1140 setIntentExtras(mIntentExtras);
1141 mIntentExtras = null;
1142
1143 // For user profile, change the contacts query URI
1144 mIsUserProfile = isUserProfile;
1145 boolean localProfileExists = false;
1146
1147 if (mIsUserProfile) {
1148 for (RawContactDelta rawContactDelta : mState) {
1149 // For profile contacts, we need a different query URI
1150 rawContactDelta.setProfileQueryUri();
1151 // Try to find a local profile contact
1152 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1153 localProfileExists = true;
1154 }
1155 }
1156 // Editor should always present a local profile for editing
1157 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
1158 // going to prune all but the one raw contact that we're trying to display by itself.
1159 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1160 mState.add(createLocalRawContactDelta());
1161 }
1162 }
1163 mExistingContactDataReady = true;
1164 bindEditors();
1165 }
1166
1167 /**
1168 * Set the enabled state of editors.
1169 */
1170 private void setEnabled(boolean enabled) {
1171 if (mEnabled != enabled) {
1172 mEnabled = enabled;
1173
1174 // Enable/disable editors
1175 if (mContent != null) {
1176 int count = mContent.getChildCount();
1177 for (int i = 0; i < count; i++) {
1178 mContent.getChildAt(i).setEnabled(enabled);
1179 }
1180 }
1181
Walter Jang7b0970f2016-09-01 10:40:19 -07001182 // Maybe invalidate the options menu
1183 final Activity activity = getActivity();
1184 if (activity != null) activity.invalidateOptionsMenu();
1185 }
1186 }
1187
1188 /**
1189 * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1190 * {@link #mState}.
1191 */
1192 private static RawContactDelta createLocalRawContactDelta() {
1193 final RawContact rawContact = new RawContact();
1194 rawContact.setAccountToLocal();
1195
1196 final RawContactDelta result = new RawContactDelta(
1197 ValuesDelta.fromAfter(rawContact.getValues()));
1198 result.setProfileQueryUri();
1199
1200 return result;
1201 }
1202
Gary Mai698cee72016-09-19 16:09:54 -07001203 private void copyReadOnlyName() {
1204 // We should only ever be doing this if we're creating a new writable contact to attach to
1205 // a read only contact.
1206 if (!isEditingReadOnlyRawContactWithNewContact()) {
1207 return;
1208 }
1209 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1210 final RawContactDelta writable = mState.get(writableIndex);
Gary Mai7b751452016-11-07 17:04:04 -08001211 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
Gary Mai698cee72016-09-19 16:09:54 -07001212 final ValuesDelta writeNameDelta = writable
1213 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1214 final ValuesDelta readNameDelta = readOnly
1215 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1216 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
1217 mCopyReadOnlyName = false;
1218 }
1219
Walter Jang7b0970f2016-09-01 10:40:19 -07001220 /**
1221 * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1222 * Contact.
1223 */
Walter Jangba59deb2015-01-26 11:23:48 -08001224 protected void bindEditors() {
Walter Jangcab3dce2015-02-09 17:48:03 -08001225 if (!isReadyToBindEditors()) {
1226 return;
1227 }
1228
Walter Jangd35e5ef2015-02-24 09:18:16 -08001229 // Add input fields for the loaded Contact
Gary Mai363af602016-09-28 10:01:23 -07001230 final RawContactEditorView editorView = getContent();
Walter Jangb6ca2722015-02-20 11:10:25 -08001231 editorView.setListener(this);
Gary Mai698cee72016-09-19 16:09:54 -07001232 if (mCopyReadOnlyName) {
1233 copyReadOnlyName();
1234 }
Gary Mai678108e2016-10-26 14:34:33 -07001235 editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
Walter Jang9a552372016-08-24 11:51:05 -07001236 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
Gary Mai5a00de32016-10-19 18:20:41 -07001237 mRawContactIdToDisplayAlone);
Gary Mai079598f2016-11-03 15:02:45 -07001238 if (isEditingReadOnlyRawContact()) {
Gary Mai15646ce2016-11-17 10:54:01 -08001239 final Toolbar toolbar = getEditorActivity().getToolbar();
1240 if (toolbar != null) {
1241 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
Gary Maid8f3da62016-11-18 11:47:20 -08001242 // Set activity title for Talkback
1243 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
John Shaobd9ef3c2016-12-15 12:42:03 -08001244 toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
Gary Mai15646ce2016-11-17 10:54:01 -08001245 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
John Shaobd9ef3c2016-12-15 12:42:03 -08001246 toolbar.getNavigationIcon().setAutoMirrored(true);
Gary Mai079598f2016-11-03 15:02:45 -07001247 }
1248 }
Walter Jangcab3dce2015-02-09 17:48:03 -08001249
Walter Jangd35e5ef2015-02-24 09:18:16 -08001250 // Set up the photo widget
Walter Jang31a74ad2015-10-02 19:17:39 -07001251 editorView.setPhotoListener(this);
Walter Jang3efae4a2015-02-18 11:12:00 -08001252 mPhotoRawContactId = editorView.getPhotoRawContactId();
Walter Jang31a74ad2015-10-02 19:17:39 -07001253 // If there is an updated full resolution photo apply it now, this will be the case if
1254 // the user selects or takes a new photo, then rotates the device.
1255 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1256 if (uri != null) {
1257 editorView.setFullSizePhoto(uri);
Walter Jang41b3ea12015-03-09 17:30:06 -07001258 }
Walter Jang3efae4a2015-02-18 11:12:00 -08001259
Walter Jangd35e5ef2015-02-24 09:18:16 -08001260 // The editor is ready now so make it visible
Gary Mai678108e2016-10-26 14:34:33 -07001261 editorView.setEnabled(mEnabled);
Walter Jangd35e5ef2015-02-24 09:18:16 -08001262 editorView.setVisibility(View.VISIBLE);
1263
1264 // Refresh the ActionBar as the visibility of the join command
1265 // Activity can be null if we have been detached from the Activity.
Walter Jangcab3dce2015-02-09 17:48:03 -08001266 invalidateOptionsMenu();
1267 }
1268
Walter Jang7b0970f2016-09-01 10:40:19 -07001269 /**
1270 * Invalidates the options menu if we are still associated with an Activity.
1271 */
1272 private void invalidateOptionsMenu() {
1273 final Activity activity = getActivity();
1274 if (activity != null) {
1275 activity.invalidateOptionsMenu();
1276 }
1277 }
1278
Walter Jangcab3dce2015-02-09 17:48:03 -08001279 private boolean isReadyToBindEditors() {
1280 if (mState.isEmpty()) {
1281 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1282 Log.v(TAG, "No data to bind editors");
1283 }
1284 return false;
1285 }
1286 if (mIsEdit && !mExistingContactDataReady) {
1287 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1288 Log.v(TAG, "Existing contact data is not ready to bind editors.");
1289 }
1290 return false;
1291 }
1292 if (mHasNewContact && !mNewContactDataReady) {
1293 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1294 Log.v(TAG, "New contact data is not ready to bind editors.");
1295 }
1296 return false;
1297 }
1298 return true;
Walter Jangba59deb2015-01-26 11:23:48 -08001299 }
1300
Walter Jang7b0970f2016-09-01 10:40:19 -07001301 /**
1302 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1303 * Some of old data are reused with new restriction enforced by the new account.
1304 *
1305 * @param oldState Old data being edited.
1306 * @param oldAccount Old account associated with oldState.
1307 * @param newAccount New account to be used.
1308 */
1309 private void rebindEditorsForNewContact(
1310 RawContactDelta oldState, AccountWithDataSet oldAccount,
1311 AccountWithDataSet newAccount) {
1312 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1313 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1314 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1315
Gary Maiaebf3202016-09-22 18:11:15 -07001316 mExistingContactDataReady = false;
1317 mNewContactDataReady = false;
1318 mState = new RawContactDeltaList();
1319 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1320 isEditingUserProfile());
1321 if (mIsEdit) {
Gary Mai698cee72016-09-19 16:09:54 -07001322 setStateForExistingContact(isEditingUserProfile(), mRawContacts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001323 }
1324 }
1325
1326 //
1327 // ContactEditor
1328 //
1329
Walter Jang3f990ba2015-01-27 17:38:30 +00001330 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -07001331 public void setListener(Listener listener) {
1332 mListener = listener;
1333 }
1334
1335 @Override
1336 public void load(String action, Uri lookupUri, Bundle intentExtras) {
1337 mAction = action;
1338 mLookupUri = lookupUri;
1339 mIntentExtras = intentExtras;
1340
1341 if (mIntentExtras != null) {
1342 mAutoAddToDefaultGroup =
1343 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1344 mNewLocalProfile =
1345 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1346 mDisableDeleteMenuOption =
1347 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1348 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1349 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1350 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1351 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1352 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1353 }
Gary Maia6c80b32016-09-30 16:34:55 -07001354 mRawContactIdToDisplayAlone = mIntentExtras
1355 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001356 }
1357 }
1358
1359 @Override
1360 public void setIntentExtras(Bundle extras) {
Gary Mai5336e6e2016-10-23 14:17:03 -07001361 getContent().setIntentExtras(extras);
Walter Jang7b0970f2016-09-01 10:40:19 -07001362 }
1363
1364 @Override
1365 public void onJoinCompleted(Uri uri) {
1366 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1367 }
1368
James Laskeye5a140a2016-10-18 15:43:42 -07001369
1370 private String getNameToDisplay(Uri contactUri) {
Gary Maic000d2e2016-11-18 13:51:17 -08001371 // The contact has been deleted or the uri is otherwise no longer right.
1372 if (contactUri == null) {
1373 return null;
1374 }
James Laskeye5a140a2016-10-18 15:43:42 -07001375 final ContentResolver resolver = mContext.getContentResolver();
1376 final Cursor cursor = resolver.query(contactUri, new String[]{
1377 ContactsContract.Contacts.DISPLAY_NAME,
1378 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
James Laskeye5a140a2016-10-18 15:43:42 -07001379
Gary Maia4adae12016-10-23 13:47:17 -07001380 if (cursor != null) {
1381 try {
1382 if (cursor.moveToFirst()) {
1383 final String displayName = cursor.getString(0);
1384 final String displayNameAlt = cursor.getString(1);
1385 cursor.close();
1386 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1387 new ContactsPreferences(mContext));
1388 }
1389 } finally {
1390 cursor.close();
1391 }
1392 }
James Laskeye5a140a2016-10-18 15:43:42 -07001393 return null;
1394 }
1395
1396
Walter Jang7b0970f2016-09-01 10:40:19 -07001397 @Override
1398 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1399 Uri contactLookupUri, Long joinContactId) {
1400 if (hadChanges) {
1401 if (saveSucceeded) {
1402 switch (saveMode) {
1403 case SaveMode.JOIN:
1404 break;
1405 case SaveMode.SPLIT:
1406 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1407 .show();
1408 break;
1409 default:
James Laskeye5a140a2016-10-18 15:43:42 -07001410 final String displayName = getNameToDisplay(contactLookupUri);
James Laskeyb1671052016-09-16 13:57:21 -07001411 final String toastMessage;
1412 if (!TextUtils.isEmpty(displayName)) {
1413 toastMessage = getResources().getString(
1414 R.string.contactSavedNamedToast, displayName);
1415 } else {
1416 toastMessage = getResources().getString(R.string.contactSavedToast);
1417 }
1418 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
Walter Jang7b0970f2016-09-01 10:40:19 -07001419 }
1420
1421 } else {
1422 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1423 }
1424 }
1425 switch (saveMode) {
1426 case SaveMode.CLOSE: {
Walter Jang581585d2016-09-21 19:21:13 -07001427 Intent resultIntent = null;
Walter Jang7b0970f2016-09-01 10:40:19 -07001428 if (saveSucceeded && contactLookupUri != null) {
1429 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1430 mContext, contactLookupUri, mLookupUri);
Walter Jangdf86ede2016-10-19 09:48:29 -07001431 if (Flags.getInstance().getBoolean(Experiments.CONTACT_SHEET)) {
Walter Jang581585d2016-09-21 19:21:13 -07001432 resultIntent = ObjectFactory.getContactSheetIntent(mContext, lookupUri);
1433 }
1434 if (resultIntent == null) {
1435 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1436 mContext, lookupUri, ScreenType.EDITOR);
1437 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
1438 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001439 } else {
1440 resultIntent = null;
1441 }
1442 // It is already saved, so prevent it from being saved again
1443 mStatus = Status.CLOSING;
1444 if (mListener != null) mListener.onSaveFinished(resultIntent);
1445 break;
1446 }
Gary Mai363af602016-09-28 10:01:23 -07001447 case SaveMode.EDITOR: {
Walter Jang7b0970f2016-09-01 10:40:19 -07001448 // It is already saved, so prevent it from being saved again
1449 mStatus = Status.CLOSING;
1450 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1451 break;
1452 }
1453 case SaveMode.JOIN:
1454 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1455 joinAggregate(joinContactId);
1456 }
1457 break;
1458 case SaveMode.RELOAD:
1459 if (saveSucceeded && contactLookupUri != null) {
1460 // If this was in INSERT, we are changing into an EDIT now.
1461 // If it already was an EDIT, we are changing to the new Uri now
1462 mState = new RawContactDeltaList();
1463 load(Intent.ACTION_EDIT, contactLookupUri, null);
1464 mStatus = Status.LOADING;
1465 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1466 }
1467 break;
1468
1469 case SaveMode.SPLIT:
1470 mStatus = Status.CLOSING;
1471 if (mListener != null) {
1472 mListener.onContactSplit(contactLookupUri);
1473 } else {
1474 Log.d(TAG, "No listener registered, can not call onSplitFinished");
1475 }
1476 break;
1477 }
1478 }
1479
1480 /**
1481 * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1482 *
1483 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1484 */
1485 private void showJoinAggregateActivity(Uri contactLookupUri) {
1486 if (contactLookupUri == null || !isAdded()) {
1487 return;
1488 }
1489
1490 mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1491 final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1492 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1493 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1494 startActivityForResult(intent, REQUEST_CODE_JOIN);
1495 }
1496
1497 //
1498 // Aggregation PopupWindow
1499 //
1500
1501 /**
1502 * Triggers an asynchronous search for aggregation suggestions.
1503 */
1504 protected void acquireAggregationSuggestions(Context context,
1505 long rawContactId, ValuesDelta valuesDelta) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001506 mAggregationSuggestionsRawContactId = rawContactId;
1507
1508 if (mAggregationSuggestionEngine == null) {
1509 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1510 mAggregationSuggestionEngine.setListener(this);
1511 mAggregationSuggestionEngine.start();
1512 }
1513
1514 mAggregationSuggestionEngine.setContactId(getContactId());
Gary Mai220d10c2016-09-23 13:56:39 -07001515 mAggregationSuggestionEngine.setAccountFilter(
1516 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
Walter Jang7b0970f2016-09-01 10:40:19 -07001517
1518 mAggregationSuggestionEngine.onNameChange(valuesDelta);
1519 }
1520
1521 /**
1522 * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1523 */
1524 private long getContactId() {
1525 for (RawContactDelta rawContact : mState) {
1526 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1527 if (contactId != null) {
1528 return contactId;
1529 }
1530 }
1531 return 0;
1532 }
1533
1534 @Override
1535 public void onAggregationSuggestionChange() {
1536 final Activity activity = getActivity();
1537 if ((activity != null && activity.isFinishing())
1538 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
1539 return;
1540 }
1541
1542 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1543
1544 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1545 return;
1546 }
1547
Gary Maida20b472016-09-20 14:46:40 -07001548 final View anchorView = getAggregationAnchorView();
Walter Jang7b0970f2016-09-01 10:40:19 -07001549 if (anchorView == null) {
1550 return; // Raw contact deleted?
1551 }
1552 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1553 mAggregationSuggestionPopup.setAnchorView(anchorView);
1554 mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1555 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1556 mAggregationSuggestionPopup.setAdapter(
1557 new AggregationSuggestionAdapter(
1558 getActivity(),
Walter Jang7b0970f2016-09-01 10:40:19 -07001559 /* listener =*/ this,
1560 mAggregationSuggestionEngine.getSuggestions()));
1561 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1562 @Override
1563 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1564 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1565 suggestionView.handleItemClickEvent();
1566 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1567 mAggregationSuggestionPopup = null;
1568 }
1569 });
1570 mAggregationSuggestionPopup.show();
1571 }
1572
1573 /**
Gary Maida20b472016-09-20 14:46:40 -07001574 * Returns the editor view that should be used as the anchor for aggregation suggestions.
Walter Jang7b0970f2016-09-01 10:40:19 -07001575 */
Gary Maida20b472016-09-20 14:46:40 -07001576 protected View getAggregationAnchorView() {
Walter Jangd35e5ef2015-02-24 09:18:16 -08001577 return getContent().getAggregationAnchorView();
1578 }
1579
Walter Jang7b0970f2016-09-01 10:40:19 -07001580 /**
1581 * Joins the suggested contact (specified by the id's of constituent raw
1582 * contacts), save all changes, and stay in the editor.
1583 */
1584 public void doJoinSuggestedContact(long[] rawContactIds) {
1585 if (!hasValidState() || mStatus != Status.EDITING) {
1586 return;
1587 }
1588
1589 mState.setJoinWithRawContacts(rawContactIds);
1590 save(SaveMode.RELOAD);
1591 }
1592
1593 @Override
Gary Mai678108e2016-10-26 14:34:33 -07001594 public void onEditAction(Uri contactLookupUri, long rawContactId) {
1595 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
Walter Jang7b0970f2016-09-01 10:40:19 -07001596 }
1597
1598 /**
Gary Mai678108e2016-10-26 14:34:33 -07001599 * Abandons the currently edited contact and switches to editing the selected raw contact,
1600 * transferring all the data there
Walter Jang7b0970f2016-09-01 10:40:19 -07001601 */
Gary Mai678108e2016-10-26 14:34:33 -07001602 public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001603 if (mListener != null) {
1604 // make sure we don't save this contact when closing down
1605 mStatus = Status.CLOSING;
Gary Mai678108e2016-10-26 14:34:33 -07001606 mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1607 getContent().getCurrentRawContactDelta().getContentValues());
Walter Jang7b0970f2016-09-01 10:40:19 -07001608 }
1609 }
1610
1611 /**
1612 * Sets group metadata on all bound editors.
1613 */
Walter Jang92f8ccc2015-02-06 10:23:37 -08001614 protected void setGroupMetaData() {
Walter Jangf10ca152015-09-22 15:23:55 -07001615 if (mGroupMetaData != null) {
1616 getContent().setGroupMetaData(mGroupMetaData);
1617 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001618 }
1619
Walter Jang7b0970f2016-09-01 10:40:19 -07001620 /**
1621 * Persist the accumulated editor deltas.
1622 *
1623 * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1624 * may be null.
1625 */
Walter Jange3373dc2015-10-27 15:35:12 -07001626 protected boolean doSaveAction(int saveMode, Long joinContactId) {
Walter Jang49ed2032015-02-11 20:09:05 -08001627 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1628 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1629 ((Activity) mContext).getClass(),
Gary Mai363af602016-09-28 10:01:23 -07001630 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
Walter Jange3373dc2015-10-27 15:35:12 -07001631 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
Wenyi Wangdd7d4562015-12-08 13:33:43 -08001632 return startSaveService(mContext, intent, saveMode);
Walter Jang49ed2032015-02-11 20:09:05 -08001633 }
1634
Walter Jang7b0970f2016-09-01 10:40:19 -07001635 private boolean startSaveService(Context context, Intent intent, int saveMode) {
1636 final boolean result = ContactSaveService.startService(
1637 context, intent, saveMode);
1638 if (!result) {
1639 onCancelEditConfirmed();
1640 }
1641 return result;
1642 }
1643
1644 //
1645 // Join Activity
1646 //
1647
1648 /**
1649 * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1650 */
Walter Jang49ed2032015-02-11 20:09:05 -08001651 protected void joinAggregate(final long contactId) {
1652 final Intent intent = ContactSaveService.createJoinContactsIntent(
Gary Mai363af602016-09-28 10:01:23 -07001653 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1654 ContactEditorActivity.ACTION_JOIN_COMPLETED);
Walter Jang49ed2032015-02-11 20:09:05 -08001655 mContext.startService(intent);
Walter Jang3f990ba2015-01-27 17:38:30 +00001656 }
Walter Jangb6ca2722015-02-20 11:10:25 -08001657
Walter Jang31a74ad2015-10-02 19:17:39 -07001658 public void removePhoto() {
1659 getContent().removePhoto();
1660 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
Walter Jang0e72ce92015-02-23 12:27:21 -08001661 }
1662
Walter Jang31a74ad2015-10-02 19:17:39 -07001663 public void updatePhoto(Uri uri) throws FileNotFoundException {
1664 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1665 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Wenyi Wang9bc9ba82015-11-17 19:37:33 -08001666 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Walter Jang31a74ad2015-10-02 19:17:39 -07001667 Toast.LENGTH_SHORT).show();
1668 return;
Walter Jang0e72ce92015-02-23 12:27:21 -08001669 }
Walter Jang31a74ad2015-10-02 19:17:39 -07001670 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1671 getContent().updatePhoto(uri);
Walter Jang0e72ce92015-02-23 12:27:21 -08001672 }
1673
Gary Maida20b472016-09-20 14:46:40 -07001674 public void setPrimaryPhoto() {
1675 getContent().setPrimaryPhoto();
Walter Jang0e72ce92015-02-23 12:27:21 -08001676 }
1677
1678 @Override
Walter Jang151f3e62015-02-26 15:29:40 -08001679 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1680 final Activity activity = getActivity();
1681 if (activity == null || activity.isFinishing()) {
1682 return;
1683 }
Walter Jang45b86d52015-10-15 15:23:16 -07001684 acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
Walter Jang151f3e62015-02-26 15:29:40 -08001685 }
1686
Walter Jang5a7a23b2015-03-06 10:54:26 -08001687 @Override
Walter Jang708ea9e2015-09-10 15:42:05 -07001688 public void onRebindEditorsForNewContact(RawContactDelta oldState,
1689 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1690 mNewContactAccountChanged = true;
1691 mAccountWithDataSet = newAccount;
1692 rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1693 }
1694
Walter Jang79658e12015-09-24 10:36:26 -07001695 @Override
1696 public void onBindEditorsFailed() {
1697 final Activity activity = getActivity();
1698 if (activity != null && !activity.isFinishing()) {
Gary Mai363af602016-09-28 10:01:23 -07001699 Toast.makeText(activity, R.string.editor_failed_to_load,
Walter Jang79658e12015-09-24 10:36:26 -07001700 Toast.LENGTH_SHORT).show();
1701 activity.setResult(Activity.RESULT_CANCELED);
1702 activity.finish();
1703 }
1704 }
1705
Walter Jangd6753152015-10-02 09:23:13 -07001706 @Override
1707 public void onEditorsBound() {
Wenyi Wang3cb77bb2016-07-27 17:39:03 -07001708 final Activity activity = getActivity();
1709 if (activity == null || activity.isFinishing()) {
1710 return;
1711 }
Walter Jangd6753152015-10-02 09:23:13 -07001712 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1713 }
1714
Walter Jang31a74ad2015-10-02 19:17:39 -07001715 @Override
1716 public void onPhotoEditorViewClicked() {
Walter Jang3f18d612015-10-07 16:01:05 -07001717 // For contacts composed of a single writable raw contact, or raw contacts have no more
1718 // than 1 photo, clicking the photo view simply opens the source photo dialog
Walter Jang31a74ad2015-10-02 19:17:39 -07001719 getEditorActivity().changePhoto(getPhotoMode());
1720 }
1721
1722 private int getPhotoMode() {
Gary Maida20b472016-09-20 14:46:40 -07001723 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1724 : PhotoActionPopup.Modes.NO_PHOTO;
Walter Jang31a74ad2015-10-02 19:17:39 -07001725 }
1726
Gary Mai363af602016-09-28 10:01:23 -07001727 private ContactEditorActivity getEditorActivity() {
1728 return (ContactEditorActivity) getActivity();
Walter Jang31a74ad2015-10-02 19:17:39 -07001729 }
1730
Gary Mai363af602016-09-28 10:01:23 -07001731 private RawContactEditorView getContent() {
1732 return (RawContactEditorView) mContent;
Walter Jang3efae4a2015-02-18 11:12:00 -08001733 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001734}