blob: ccb3ff941a3fac3eed5afe3903c83898d7673383 [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;
Gary Maic40f3e92016-12-20 18:22:54 -080044import android.support.v7.widget.Toolbar;
Walter Jange3945952015-10-27 12:44:54 -070045import android.text.TextUtils;
Walter Jangcab3dce2015-02-09 17:48:03 -080046import android.util.Log;
Walter Jang3f990ba2015-01-27 17:38:30 +000047import android.view.LayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -070048import android.view.Menu;
49import android.view.MenuInflater;
Walter Jangc90cc152015-06-19 14:15:08 -070050import android.view.MenuItem;
Walter Jang3f990ba2015-01-27 17:38:30 +000051import android.view.View;
52import android.view.ViewGroup;
Walter Jang7b0970f2016-09-01 10:40:19 -070053import android.widget.AdapterView;
54import android.widget.BaseAdapter;
yoichi kakimoto5ed462a2017-09-04 19:20:13 +090055import android.widget.EditText;
Walter Jang3f990ba2015-01-27 17:38:30 +000056import android.widget.LinearLayout;
Walter Jang7b0970f2016-09-01 10:40:19 -070057import android.widget.ListPopupWindow;
Walter Jang79658e12015-09-24 10:36:26 -070058import android.widget.Toast;
Walter Jang3f990ba2015-01-27 17:38:30 +000059
Walter Jang7b0970f2016-09-01 10:40:19 -070060import com.android.contacts.ContactSaveService;
61import 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 Mai1e899dc2017-02-07 15:08:53 -080067import com.android.contacts.activities.RequestPermissionsActivity;
Gary Mai0a49afa2016-12-05 15:53:58 -080068import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
69import com.android.contacts.group.GroupUtil;
70import com.android.contacts.list.UiIntentActions;
Gary Mai69c182a2016-12-05 13:07:03 -080071import com.android.contacts.logging.ScreenEvent.ScreenType;
72import com.android.contacts.model.AccountTypeManager;
73import com.android.contacts.model.Contact;
74import com.android.contacts.model.ContactLoader;
75import com.android.contacts.model.RawContact;
76import com.android.contacts.model.RawContactDelta;
77import com.android.contacts.model.RawContactDeltaList;
78import com.android.contacts.model.RawContactModifier;
79import com.android.contacts.model.ValuesDelta;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -080080import com.android.contacts.model.account.AccountInfo;
Gary Mai69c182a2016-12-05 13:07:03 -080081import com.android.contacts.model.account.AccountType;
82import com.android.contacts.model.account.AccountWithDataSet;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -080083import com.android.contacts.model.account.AccountsLoader;
Gary Mai69c182a2016-12-05 13:07:03 -080084import com.android.contacts.preference.ContactsPreferences;
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;
Gary Mai0a49afa2016-12-05 15:53:58 -080087import com.android.contacts.util.ContactDisplayUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070088import com.android.contacts.util.ContactPhotoUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080089import com.android.contacts.util.ImplicitIntentsUtil;
90import com.android.contacts.util.MaterialColorMapUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070091import com.android.contacts.util.UiClosables;
Gary Maia4adae12016-10-23 13:47:17 -070092import com.android.contactsbind.HelpUtils;
Walter Jang8d26c0a2017-02-08 10:56:04 -080093
Marcus Hagerott807e6202016-12-21 08:59:50 -080094import com.google.common.base.Preconditions;
Walter Jang7b0970f2016-09-01 10:40:19 -070095import com.google.common.collect.ImmutableList;
96import com.google.common.collect.Lists;
97
Walter Jang3efae4a2015-02-18 11:12:00 -080098import java.io.FileNotFoundException;
Walter Jang31a74ad2015-10-02 19:17:39 -070099import java.util.ArrayList;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800100import java.util.Collections;
Walter Jang7b0970f2016-09-01 10:40:19 -0700101import java.util.HashSet;
102import java.util.Iterator;
103import java.util.List;
yoichi kakimoto5ed462a2017-09-04 19:20:13 +0900104import java.util.Locale;
Walter Jang7b0970f2016-09-01 10:40:19 -0700105import java.util.Set;
Walter Jang3efae4a2015-02-18 11:12:00 -0800106
Walter Jang3f990ba2015-01-27 17:38:30 +0000107/**
108 * Contact editor with only the most important fields displayed initially.
109 */
Gary Mai363af602016-09-28 10:01:23 -0700110public class ContactEditorFragment extends Fragment implements
Walter Jang7b0970f2016-09-01 10:40:19 -0700111 ContactEditor, SplitContactConfirmationDialogFragment.Listener,
112 JoinContactConfirmationDialogFragment.Listener,
113 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
114 CancelEditDialogFragment.Listener,
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800115 RawContactEditorView.Listener, PhotoEditorView.Listener,
116 AccountsLoader.AccountsListener {
Walter Jang3f990ba2015-01-27 17:38:30 +0000117
Walter Jang7b0970f2016-09-01 10:40:19 -0700118 static final String TAG = "ContactEditor";
119
120 private static final int LOADER_CONTACT = 1;
121 private static final int LOADER_GROUPS = 2;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800122 private static final int LOADER_ACCOUNTS = 3;
Walter Jang7b0970f2016-09-01 10:40:19 -0700123
Walter Jang3efae4a2015-02-18 11:12:00 -0800124 private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
Walter Jang28a27272015-09-19 16:06:08 -0700125 private static final String KEY_UPDATED_PHOTOS = "updated_photos";
Walter Jang3efae4a2015-02-18 11:12:00 -0800126
Walter Jang7b0970f2016-09-01 10:40:19 -0700127 private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
128 add(Intent.ACTION_EDIT);
129 add(Intent.ACTION_INSERT);
Gary Mai363af602016-09-28 10:01:23 -0700130 add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
Walter Jang7b0970f2016-09-01 10:40:19 -0700131 }};
132
133 private static final String KEY_ACTION = "action";
134 private static final String KEY_URI = "uri";
135 private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
136 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
137 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
138 private static final String KEY_MATERIAL_PALETTE = "materialPalette";
Gary Maic135a5d2016-12-19 11:13:46 -0800139 private static final String KEY_ACCOUNT = "saveToAccount";
Walter Jang7b0970f2016-09-01 10:40:19 -0700140 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
141
142 private static final String KEY_RAW_CONTACTS = "rawContacts";
143
144 private static final String KEY_EDIT_STATE = "state";
145 private static final String KEY_STATUS = "status";
146
147 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
148 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
149
150 private static final String KEY_IS_EDIT = "isEdit";
151 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
152
Walter Jang7b0970f2016-09-01 10:40:19 -0700153 private static final String KEY_IS_USER_PROFILE = "isUserProfile";
154
155 private static final String KEY_ENABLED = "enabled";
156
157 // Aggregation PopupWindow
158 private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
159 "aggregationSuggestionsRawContactId";
160
161 // Join Activity
162 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
163
Gary Mai698cee72016-09-19 16:09:54 -0700164 private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
165 private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
Walter Jang7b0970f2016-09-01 10:40:19 -0700166
167 protected static final int REQUEST_CODE_JOIN = 0;
168 protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700169
Walter Jang7b0970f2016-09-01 10:40:19 -0700170 /**
171 * An intent extra that forces the editor to add the edited contact
172 * to the default group (e.g. "My Contacts").
173 */
174 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
175
176 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
177
178 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
179 "disableDeleteMenuOption";
180
181 /**
182 * Intent key to pass the photo palette primary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700183 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700184 */
185 public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
186 "material_palette_primary_color";
187
188 /**
189 * Intent key to pass the photo palette secondary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700190 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700191 */
192 public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
193 "material_palette_secondary_color";
194
195 /**
196 * Intent key to pass the ID of the photo to display on the editor.
197 */
Gary Maida20b472016-09-20 14:46:40 -0700198 // TODO: This can be cleaned up if we decide to not pass the photo id through
199 // QuickContactActivity.
Walter Jang7b0970f2016-09-01 10:40:19 -0700200 public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
201
202 /**
Gary Maia6c80b32016-09-30 16:34:55 -0700203 * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
204 * by itself.
205 */
206 public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
207 "raw_contact_id_to_display_alone";
208
209 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700210 * Intent extra to specify a {@link ContactEditor.SaveMode}.
211 */
212 public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
213
214 /**
215 * Intent extra key for the contact ID to join the current contact to after saving.
216 */
217 public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
218
219 /**
220 * Callbacks for Activities that host contact editors Fragments.
221 */
222 public interface Listener {
223
224 /**
225 * Contact was not found, so somehow close this fragment. This is raised after a contact
226 * is removed via Menu/Delete
227 */
228 void onContactNotFound();
229
230 /**
231 * Contact was split, so we can close now.
232 *
233 * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
234 * The editor tries best to chose the most natural contact here.
235 */
236 void onContactSplit(Uri newLookupUri);
237
238 /**
239 * User has tapped Revert, close the fragment now.
240 */
241 void onReverted();
242
243 /**
244 * Contact was saved and the Fragment can now be closed safely.
245 */
246 void onSaveFinished(Intent resultIntent);
247
248 /**
Gary Mai678108e2016-10-26 14:34:33 -0700249 * User switched to editing a different raw contact (a suggestion from the
Walter Jang7b0970f2016-09-01 10:40:19 -0700250 * aggregation engine).
251 */
Gary Mai678108e2016-10-26 14:34:33 -0700252 void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
Walter Jang7b0970f2016-09-01 10:40:19 -0700253 ArrayList<ContentValues> contentValues);
254
255 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700256 * User has requested that contact be deleted.
257 */
258 void onDeleteRequested(Uri contactUri);
259 }
260
261 /**
262 * Adapter for aggregation suggestions displayed in a PopupWindow when
263 * editor fields change.
264 */
265 private static final class AggregationSuggestionAdapter extends BaseAdapter {
266 private final LayoutInflater mLayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -0700267 private final AggregationSuggestionView.Listener mListener;
268 private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
269
Gary Mai678108e2016-10-26 14:34:33 -0700270 public AggregationSuggestionAdapter(Activity activity,
Walter Jang7b0970f2016-09-01 10:40:19 -0700271 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
272 mLayoutInflater = activity.getLayoutInflater();
Walter Jang7b0970f2016-09-01 10:40:19 -0700273 mListener = listener;
274 mSuggestions = suggestions;
275 }
276
277 @Override
278 public View getView(int position, View convertView, ViewGroup parent) {
279 final Suggestion suggestion = (Suggestion) getItem(position);
280 final AggregationSuggestionView suggestionView =
281 (AggregationSuggestionView) mLayoutInflater.inflate(
282 R.layout.aggregation_suggestions_item, null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700283 suggestionView.setListener(mListener);
284 suggestionView.bindSuggestion(suggestion);
285 return suggestionView;
286 }
287
288 @Override
289 public long getItemId(int position) {
290 return position;
291 }
292
293 @Override
294 public Object getItem(int position) {
295 return mSuggestions.get(position);
296 }
297
298 @Override
299 public int getCount() {
300 return mSuggestions.size();
301 }
302 }
303
304 protected Context mContext;
305 protected Listener mListener;
306
307 //
308 // Views
309 //
310 protected LinearLayout mContent;
Walter Jang7b0970f2016-09-01 10:40:19 -0700311 protected ListPopupWindow mAggregationSuggestionPopup;
312
313 //
314 // Parameters passed in on {@link #load}
315 //
316 protected String mAction;
317 protected Uri mLookupUri;
318 protected Bundle mIntentExtras;
319 protected boolean mAutoAddToDefaultGroup;
320 protected boolean mDisableDeleteMenuOption;
321 protected boolean mNewLocalProfile;
322 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang7b0970f2016-09-01 10:40:19 -0700323
324 //
325 // Helpers
326 //
327 protected ContactEditorUtils mEditorUtils;
328 protected RawContactDeltaComparator mComparator;
329 protected ViewIdGenerator mViewIdGenerator;
330 private AggregationSuggestionEngine mAggregationSuggestionEngine;
331
332 //
333 // Loaded data
334 //
335 // Used to store existing contact data so it can be re-applied during a rebind call,
336 // i.e. account switch.
Gary Mai7b751452016-11-07 17:04:04 -0800337 protected Contact mContact;
Walter Jang7b0970f2016-09-01 10:40:19 -0700338 protected ImmutableList<RawContact> mRawContacts;
339 protected Cursor mGroupMetaData;
340
341 //
342 // Editor state
343 //
344 protected RawContactDeltaList mState;
345 protected int mStatus;
346 protected long mRawContactIdToDisplayAlone = -1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700347
348 // Whether to show the new contact blank form and if it's corresponding delta is ready.
349 protected boolean mHasNewContact;
350 protected AccountWithDataSet mAccountWithDataSet;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800351 protected List<AccountInfo> mWritableAccounts = Collections.emptyList();
Walter Jang7b0970f2016-09-01 10:40:19 -0700352 protected boolean mNewContactDataReady;
353 protected boolean mNewContactAccountChanged;
354
355 // Whether it's an edit of existing contact and if it's corresponding delta is ready.
356 protected boolean mIsEdit;
357 protected boolean mExistingContactDataReady;
358
359 // Whether we are editing the "me" profile
360 protected boolean mIsUserProfile;
361
Walter Jang7b0970f2016-09-01 10:40:19 -0700362 // Whether editor views and options menu items should be enabled
363 private boolean mEnabled = true;
364
365 // Aggregation PopupWindow
366 private long mAggregationSuggestionsRawContactId;
367
368 // Join Activity
369 protected long mContactIdForJoin;
370
371 // 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 -0700372 protected long mReadOnlyDisplayNameId;
373 protected boolean mCopyReadOnlyName;
Walter Jang7b0970f2016-09-01 10:40:19 -0700374
375 /**
376 * The contact data loader listener.
377 */
378 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
379 new LoaderManager.LoaderCallbacks<Contact>() {
380
381 protected long mLoaderStartTime;
382
383 @Override
384 public Loader<Contact> onCreateLoader(int id, Bundle args) {
385 mLoaderStartTime = SystemClock.elapsedRealtime();
Gary Maie4874662016-09-26 11:42:54 -0700386 return new ContactLoader(mContext, mLookupUri,
387 /* postViewNotification */ true,
388 /* loadGroupMetaData */ true);
Walter Jang7b0970f2016-09-01 10:40:19 -0700389 }
390
391 @Override
392 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
393 final long loaderCurrentTime = SystemClock.elapsedRealtime();
Wenyi Wang57a0e982017-03-24 16:02:44 -0700394 if (Log.isLoggable(TAG, Log.VERBOSE)) {
395 Log.v(TAG,
396 "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
397 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700398 if (!contact.isLoaded()) {
399 // Item has been deleted. Close activity without saving again.
400 Log.i(TAG, "No contact found. Closing activity");
401 mStatus = Status.CLOSING;
402 if (mListener != null) mListener.onContactNotFound();
403 return;
404 }
405
406 mStatus = Status.EDITING;
407 mLookupUri = contact.getLookupUri();
408 final long setDataStartTime = SystemClock.elapsedRealtime();
409 setState(contact);
Walter Jang7b0970f2016-09-01 10:40:19 -0700410 final long setDataEndTime = SystemClock.elapsedRealtime();
Wenyi Wang57a0e982017-03-24 16:02:44 -0700411 if (Log.isLoggable(TAG, Log.VERBOSE)) {
412 Log.v(TAG, "Time needed for setting UI: "
413 + (setDataEndTime - setDataStartTime));
414 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700415 }
416
417 @Override
418 public void onLoaderReset(Loader<Contact> loader) {
419 }
420 };
421
422 /**
423 * The groups meta data loader listener.
424 */
425 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
426 new LoaderManager.LoaderCallbacks<Cursor>() {
427
428 @Override
429 public CursorLoader onCreateLoader(int id, Bundle args) {
Gary Mai5c1bff22016-09-30 15:10:25 -0700430 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
431 GroupUtil.ALL_GROUPS_SELECTION);
Walter Jang7b0970f2016-09-01 10:40:19 -0700432 }
433
434 @Override
435 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
436 mGroupMetaData = data;
437 setGroupMetaData();
438 }
439
440 @Override
441 public void onLoaderReset(Loader<Cursor> loader) {
442 }
443 };
444
Walter Jang3efae4a2015-02-18 11:12:00 -0800445 private long mPhotoRawContactId;
Walter Jang28a27272015-09-19 16:06:08 -0700446 private Bundle mUpdatedPhotos = new Bundle();
Walter Jang3efae4a2015-02-18 11:12:00 -0800447
448 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700449 public Context getContext() {
450 return getActivity();
451 }
452
453 @Override
454 public void onAttach(Activity activity) {
455 super.onAttach(activity);
456 mContext = activity;
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700457 mEditorUtils = ContactEditorUtils.create(mContext);
Walter Jang7b0970f2016-09-01 10:40:19 -0700458 mComparator = new RawContactDeltaComparator(mContext);
459 }
460
461 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800462 public void onCreate(Bundle savedState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700463 if (savedState != null) {
464 // Restore mUri before calling super.onCreate so that onInitializeLoaders
465 // would already have a uri and an action to work with
466 mAction = savedState.getString(KEY_ACTION);
467 mLookupUri = savedState.getParcelable(KEY_URI);
468 }
469
Walter Jang3efae4a2015-02-18 11:12:00 -0800470 super.onCreate(savedState);
471
Walter Jang7b0970f2016-09-01 10:40:19 -0700472 if (savedState == null) {
473 mViewIdGenerator = new ViewIdGenerator();
474
475 // mState can still be null because it may not have have finished loading before
476 // onSaveInstanceState was called.
477 mState = new RawContactDeltaList();
478 } else {
479 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
480
481 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
482 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
483 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
484 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
Gary Maic135a5d2016-12-19 11:13:46 -0800485 mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
Walter Jang7b0970f2016-09-01 10:40:19 -0700486 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
487 KEY_RAW_CONTACTS));
488 // NOTE: mGroupMetaData is not saved/restored
489
490 // Read state from savedState. No loading involved here
491 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
492 mStatus = savedState.getInt(KEY_STATUS);
Walter Jang7b0970f2016-09-01 10:40:19 -0700493
494 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
495 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
496
497 mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
498 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
499
500 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
501
Walter Jang7b0970f2016-09-01 10:40:19 -0700502 mEnabled = savedState.getBoolean(KEY_ENABLED);
503
504 // Aggregation PopupWindow
505 mAggregationSuggestionsRawContactId = savedState.getLong(
506 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
507
508 // Join Activity
509 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
510
Gary Mai698cee72016-09-19 16:09:54 -0700511 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
512 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700513
Walter Jang3efae4a2015-02-18 11:12:00 -0800514 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
Walter Jang28a27272015-09-19 16:06:08 -0700515 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
Walter Jang3efae4a2015-02-18 11:12:00 -0800516 }
517 }
518
Walter Jang3f990ba2015-01-27 17:38:30 +0000519 @Override
Walter Jang3f990ba2015-01-27 17:38:30 +0000520 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Walter Janged8f6c92015-01-30 16:07:47 -0800521 setHasOptionsMenu(true);
522
Walter Jang3f990ba2015-01-27 17:38:30 +0000523 final View view = inflater.inflate(
Gary Mai363af602016-09-28 10:01:23 -0700524 R.layout.contact_editor_fragment, container, false);
Walter Jangf5dfea42015-09-16 12:30:36 -0700525 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
Walter Jang3f990ba2015-01-27 17:38:30 +0000526 return view;
527 }
528
Walter Janged8f6c92015-01-30 16:07:47 -0800529 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700530 public void onActivityCreated(Bundle savedInstanceState) {
531 super.onActivityCreated(savedInstanceState);
532
533 validateAction(mAction);
534
535 if (mState.isEmpty()) {
536 // The delta list may not have finished loading before orientation change happens.
537 // In this case, there will be a saved state but deltas will be missing. Reload from
538 // database.
539 if (Intent.ACTION_EDIT.equals(mAction)) {
540 // Either
541 // 1) orientation change but load never finished.
542 // 2) not an orientation change so data needs to be loaded for first time.
543 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
544 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
545 }
546 } else {
547 // Orientation change, we already have mState, it was loaded by onCreate
548 bindEditors();
549 }
550
551 // Handle initial actions only when existing state missing
552 if (savedInstanceState == null) {
Marcus Hagerott807e6202016-12-21 08:59:50 -0800553 if (mIntentExtras != null) {
554 final Account account = mIntentExtras == null ? null :
555 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
556 final String dataSet = mIntentExtras == null ? null :
557 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
558 mAccountWithDataSet = account != null
559 ? new AccountWithDataSet(account.name, account.type, dataSet)
560 : mIntentExtras.<AccountWithDataSet>getParcelable(
561 ContactEditorActivity.EXTRA_ACCOUNT_WITH_DATA_SET);
Walter Jang7b0970f2016-09-01 10:40:19 -0700562 }
563
564 if (Intent.ACTION_EDIT.equals(mAction)) {
565 mIsEdit = true;
566 } else if (Intent.ACTION_INSERT.equals(mAction)) {
567 mHasNewContact = true;
568 if (mAccountWithDataSet != null) {
569 createContact(mAccountWithDataSet);
Marcus Hagerott807e6202016-12-21 08:59:50 -0800570 } // else wait for accounts to be loaded
Walter Jang7b0970f2016-09-01 10:40:19 -0700571 }
572 }
Marcus Hagerott807e6202016-12-21 08:59:50 -0800573
574 if (mHasNewContact) {
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800575 AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter());
Marcus Hagerott807e6202016-12-21 08:59:50 -0800576 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700577 }
578
579 /**
580 * Checks if the requested action is valid.
581 *
582 * @param action The action to test.
583 * @throws IllegalArgumentException when the action is invalid.
584 */
585 private static void validateAction(String action) {
586 if (VALID_INTENT_ACTIONS.contains(action)) {
587 return;
588 }
589 throw new IllegalArgumentException(
590 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
591 }
592
593 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800594 public void onSaveInstanceState(Bundle outState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700595 outState.putString(KEY_ACTION, mAction);
596 outState.putParcelable(KEY_URI, mLookupUri);
597 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
598 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
599 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
600 if (mMaterialPalette != null) {
601 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
602 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700603 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
604
605 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
606 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
607 // NOTE: mGroupMetaData is not saved
608
Gary Mai36ceb422016-10-17 14:04:17 -0700609 outState.putParcelable(KEY_EDIT_STATE, mState);
Walter Jang7b0970f2016-09-01 10:40:19 -0700610 outState.putInt(KEY_STATUS, mStatus);
611 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
612 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
613 outState.putBoolean(KEY_IS_EDIT, mIsEdit);
614 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
Gary Maic135a5d2016-12-19 11:13:46 -0800615 outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
Walter Jang7b0970f2016-09-01 10:40:19 -0700616 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
617
Walter Jang7b0970f2016-09-01 10:40:19 -0700618 outState.putBoolean(KEY_ENABLED, mEnabled);
619
620 // Aggregation PopupWindow
621 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
622 mAggregationSuggestionsRawContactId);
623
624 // Join Activity
625 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
626
Gary Mai698cee72016-09-19 16:09:54 -0700627 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
628 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
Walter Jang7b0970f2016-09-01 10:40:19 -0700629
Walter Jang3efae4a2015-02-18 11:12:00 -0800630 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
Walter Jang28a27272015-09-19 16:06:08 -0700631 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
Walter Jang3efae4a2015-02-18 11:12:00 -0800632 super.onSaveInstanceState(outState);
633 }
634
635 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700636 public void onStop() {
637 super.onStop();
638 UiClosables.closeQuietly(mAggregationSuggestionPopup);
639 }
640
641 @Override
642 public void onDestroy() {
643 super.onDestroy();
644 if (mAggregationSuggestionEngine != null) {
645 mAggregationSuggestionEngine.quit();
646 }
647 }
648
649 @Override
650 public void onActivityResult(int requestCode, int resultCode, Intent data) {
651 switch (requestCode) {
652 case REQUEST_CODE_JOIN: {
653 // Ignore failed requests
654 if (resultCode != Activity.RESULT_OK) return;
655 if (data != null) {
656 final long contactId = ContentUris.parseId(data.getData());
657 if (hasPendingChanges()) {
658 // Ask the user if they want to save changes before doing the join
659 JoinContactConfirmationDialogFragment.show(this, contactId);
660 } else {
661 // Do the join immediately
662 joinAggregate(contactId);
663 }
664 }
665 break;
666 }
667 case REQUEST_CODE_ACCOUNTS_CHANGED: {
668 // Bail if the account selector was not successful.
Marcus Hagerott807e6202016-12-21 08:59:50 -0800669 if (resultCode != Activity.RESULT_OK || data == null ||
670 !data.hasExtra(Intents.Insert.EXTRA_ACCOUNT)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700671 if (mListener != null) {
672 mListener.onReverted();
673 }
674 return;
675 }
Marcus Hagerott807e6202016-12-21 08:59:50 -0800676 AccountWithDataSet account = data.getParcelableExtra(
677 Intents.Insert.EXTRA_ACCOUNT);
678 createContact(account);
Walter Jang7b0970f2016-09-01 10:40:19 -0700679 break;
680 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700681 }
682 }
683
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800684 @Override
685 public void onAccountsLoaded(List<AccountInfo> data) {
686 mWritableAccounts = data;
687 // The user may need to select a new account to save to
688 if (mAccountWithDataSet == null && mHasNewContact) {
689 selectAccountAndCreateContact();
690 }
691
692 final RawContactEditorView view = getContent();
693 if (view == null) {
694 return;
695 }
696 view.setAccounts(data);
697 if (mAccountWithDataSet == null && view.getCurrentRawContactDelta() == null) {
698 return;
699 }
700
701 final AccountWithDataSet account = mAccountWithDataSet != null
702 ? mAccountWithDataSet
703 : view.getCurrentRawContactDelta().getAccountWithDataSet();
704
705 // The current account was removed
706 if (!AccountInfo.contains(data, account) && !data.isEmpty()) {
707 if (isReadyToBindEditors()) {
708 onRebindEditorsForNewContact(getContent().getCurrentRawContactDelta(),
709 account, data.get(0).getAccount());
710 } else {
711 mAccountWithDataSet = data.get(0).getAccount();
712 }
713 }
714 }
715
Walter Jang7b0970f2016-09-01 10:40:19 -0700716 //
717 // Options menu
718 //
719
Walter Jang7b0970f2016-09-01 10:40:19 -0700720 @Override
721 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
722 inflater.inflate(R.menu.edit_contact, menu);
723 }
724
725 @Override
726 public void onPrepareOptionsMenu(Menu menu) {
727 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
728 // because the custom action bar contains the "save" button now (not the overflow menu).
729 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
730 final MenuItem saveMenu = menu.findItem(R.id.menu_save);
731 final MenuItem splitMenu = menu.findItem(R.id.menu_split);
732 final MenuItem joinMenu = menu.findItem(R.id.menu_join);
Walter Jang7b0970f2016-09-01 10:40:19 -0700733 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
734
Gary Mai5eda2572016-10-11 18:01:32 -0700735 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
736 // on a raw contact level.
737 joinMenu.setVisible(false);
738 splitMenu.setVisible(false);
739 deleteMenu.setVisible(false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700740 // Save menu is invisible when there's only one read only contact in the editor.
Gary Maid7faa652016-10-03 11:53:39 -0700741 saveMenu.setVisible(!isEditingReadOnlyRawContact());
Walter Jang7b0970f2016-09-01 10:40:19 -0700742 if (saveMenu.isVisible()) {
743 // Since we're using a custom action layout we have to manually hook up the handler.
744 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
745 @Override
746 public void onClick(View v) {
747 onOptionsItemSelected(saveMenu);
748 }
749 });
750 }
751
Walter Jang7b0970f2016-09-01 10:40:19 -0700752 int size = menu.size();
753 for (int i = 0; i < size; i++) {
754 menu.getItem(i).setEnabled(mEnabled);
755 }
756 }
757
758 @Override
Walter Jangc90cc152015-06-19 14:15:08 -0700759 public boolean onOptionsItemSelected(MenuItem item) {
760 if (item.getItemId() == android.R.id.home) {
761 return revert();
762 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700763
764 final Activity activity = getActivity();
765 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
766 // If we no longer are attached to a running activity want to
767 // drain this event.
768 return true;
769 }
770
Marcus Hagerottb697ed72016-12-20 15:02:56 -0800771 final int id = item.getItemId();
772 if (id == R.id.menu_save) {
773 return save(SaveMode.CLOSE);
774 } else if (id == R.id.menu_delete) {
775 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
776 return true;
777 } else if (id == R.id.menu_split) {
778 return doSplitContactAction();
779 } else if (id == R.id.menu_join) {
780 return doJoinContactAction();
781 } else if (id == R.id.menu_help) {
782 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
783 return true;
Walter Jang7b0970f2016-09-01 10:40:19 -0700784 }
785
786 return false;
Walter Jangc90cc152015-06-19 14:15:08 -0700787 }
788
789 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700790 public boolean revert() {
791 if (mState.isEmpty() || !hasPendingChanges()) {
792 onCancelEditConfirmed();
793 } else {
794 CancelEditDialogFragment.show(this);
795 }
796 return true;
797 }
798
799 @Override
800 public void onCancelEditConfirmed() {
801 // When this Fragment is closed we don't want it to auto-save
802 mStatus = Status.CLOSING;
803 if (mListener != null) {
804 mListener.onReverted();
805 }
806 }
807
808 @Override
809 public void onSplitContactConfirmed(boolean hasPendingChanges) {
810 if (mState.isEmpty()) {
811 // This may happen when this Fragment is recreated by the system during users
812 // confirming the split action (and thus this method is called just before onCreate()),
813 // for example.
814 Log.e(TAG, "mState became null during the user's confirming split action. " +
815 "Cannot perform the save action.");
816 return;
817 }
818
819 if (!hasPendingChanges && mHasNewContact) {
820 // If the user didn't add anything new, we don't want to split out the newly created
821 // raw contact into a name-only contact so remove them.
822 final Iterator<RawContactDelta> iterator = mState.iterator();
823 while (iterator.hasNext()) {
824 final RawContactDelta rawContactDelta = iterator.next();
825 if (rawContactDelta.getRawContactId() < 0) {
826 iterator.remove();
827 }
828 }
829 }
830 mState.markRawContactsForSplitting();
831 save(SaveMode.SPLIT);
832 }
833
Gary Maib9065dd2016-11-08 10:49:00 -0800834 @Override
835 public void onSplitContactCanceled() {}
836
Walter Jang7b0970f2016-09-01 10:40:19 -0700837 private boolean doSplitContactAction() {
838 if (!hasValidState()) return false;
839
840 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
841 return true;
842 }
843
844 private boolean doJoinContactAction() {
845 if (!hasValidState() || mLookupUri == null) {
846 return false;
847 }
848
849 // If we just started creating a new contact and haven't added any data, it's too
850 // early to do a join
851 if (mState.size() == 1 && mState.get(0).isContactInsert()
852 && !hasPendingChanges()) {
853 Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
854 Toast.LENGTH_LONG).show();
855 return true;
856 }
857
858 showJoinAggregateActivity(mLookupUri);
859 return true;
860 }
861
862 @Override
863 public void onJoinContactConfirmed(long joinContactId) {
864 doSaveAction(SaveMode.JOIN, joinContactId);
865 }
866
Walter Jang7b0970f2016-09-01 10:40:19 -0700867 @Override
868 public boolean save(int saveMode) {
869 if (!hasValidState() || mStatus != Status.EDITING) {
870 return false;
871 }
872
873 // If we are about to close the editor - there is no need to refresh the data
Gary Mai363af602016-09-28 10:01:23 -0700874 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
Walter Jang7b0970f2016-09-01 10:40:19 -0700875 || saveMode == SaveMode.SPLIT) {
876 getLoaderManager().destroyLoader(LOADER_CONTACT);
877 }
878
879 mStatus = Status.SAVING;
880
881 if (!hasPendingChanges()) {
882 if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
883 // We don't have anything to save and there isn't even an existing contact yet.
884 // Nothing to do, simply go back to editing mode
885 mStatus = Status.EDITING;
886 return true;
887 }
888 onSaveCompleted(/* hadChanges =*/ false, saveMode,
889 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
890 return true;
891 }
892
893 setEnabled(false);
894
895 return doSaveAction(saveMode, /* joinContactId */ null);
896 }
897
898 //
899 // State accessor methods
900 //
901
902 /**
903 * Check if our internal {@link #mState} is valid, usually checked before
904 * performing user actions.
905 */
906 private boolean hasValidState() {
907 return mState.size() > 0;
908 }
909
910 private boolean isEditingUserProfile() {
911 return mNewLocalProfile || mIsUserProfile;
912 }
913
914 /**
Gary Mai5a00de32016-10-19 18:20:41 -0700915 * Whether the contact being edited is composed of read-only raw contacts
Walter Jang7b0970f2016-09-01 10:40:19 -0700916 * aggregated with a newly created writable raw contact.
917 */
918 private boolean isEditingReadOnlyRawContactWithNewContact() {
Gary Mai5a00de32016-10-19 18:20:41 -0700919 return mHasNewContact && mState.size() > 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700920 }
921
922 /**
Gary Maid7faa652016-10-03 11:53:39 -0700923 * @return true if the single raw contact we're looking at is read-only.
924 */
925 private boolean isEditingReadOnlyRawContact() {
926 return hasValidState() && mRawContactIdToDisplayAlone > 0
927 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
928 .getAccountType(AccountTypeManager.getInstance(mContext))
929 .areContactsWritable();
930 }
931
932 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700933 * Return true if there are any edits to the current contact which need to
934 * be saved.
935 */
936 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
937 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
938 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
939 }
940
941 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700942 * Determines if changes were made in the editor that need to be saved, while taking into
943 * account that name changes are not real for read-only contacts.
944 * See go/editing-read-only-contacts
945 */
946 private boolean hasPendingChanges() {
Gary Mai698cee72016-09-19 16:09:54 -0700947 if (isEditingReadOnlyRawContactWithNewContact()) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700948 // We created a new raw contact delta with a default display name.
949 // We must test for pending changes while ignoring the default display name.
Gary Mai698cee72016-09-19 16:09:54 -0700950 final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId)
951 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
952 final ValuesDelta pendingDelta = mState
953 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
954 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700955 final Set<String> excludedMimeTypes = new HashSet<>();
956 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
957 return hasPendingRawContactChanges(excludedMimeTypes);
958 }
959 return true;
960 }
961 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
962 }
963
964 /**
Gary Mai698cee72016-09-19 16:09:54 -0700965 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
966 * of a read only delta and now we want to check if the copied delta has changes.
967 *
968 * @param before original {@link ValuesDelta}
969 * @param after copied {@link ValuesDelta}
970 * @return true if the copied {@link ValuesDelta} has all the same values in the structured
971 * name fields as the original.
972 */
973 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
Gary Mai5a00de32016-10-19 18:20:41 -0700974 if (before == after) return true;
Gary Mai698cee72016-09-19 16:09:54 -0700975 if (before == null || after == null) return false;
976 final ContentValues original = before.getBefore();
977 final ContentValues pending = after.getAfter();
978 if (original != null && pending != null) {
Gary Maia4adae12016-10-23 13:47:17 -0700979 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
Gary Mai698cee72016-09-19 16:09:54 -0700980 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
981 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
982
983 final String beforePrefix = original.getAsString(StructuredName.PREFIX);
984 final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
985 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
986
987 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
988 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
989 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
990
991 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
992 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
993 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
994
995 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
996 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
997 if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
998
999 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
1000 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
1001 return TextUtils.equals(beforeSuffix, afterSuffix);
1002 }
1003 return false;
1004 }
1005
Walter Jang7b0970f2016-09-01 10:40:19 -07001006 //
1007 // Account creation
1008 //
1009
1010 private void selectAccountAndCreateContact() {
Marcus Hagerott807e6202016-12-21 08:59:50 -08001011 Preconditions.checkNotNull(mWritableAccounts, "Accounts must be loaded first");
Walter Jang7b0970f2016-09-01 10:40:19 -07001012 // If this is a local profile, then skip the logic about showing the accounts changed
1013 // activity and create a phone-local contact.
1014 if (mNewLocalProfile) {
1015 createContact(null);
1016 return;
1017 }
1018
Marcus Hagerott807e6202016-12-21 08:59:50 -08001019 final List<AccountWithDataSet> accounts = AccountInfo.extractAccounts(mWritableAccounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001020 // If there is no default account or the accounts have changed such that we need to
1021 // prompt the user again, then launch the account prompt.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001022 if (mEditorUtils.shouldShowAccountChangedNotification(accounts)) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001023 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
1024 // Prevent a second instance from being started on rotates
Marcus Hagerott80ab7ea2017-01-03 13:19:56 -08001025 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Walter Jang7b0970f2016-09-01 10:40:19 -07001026 mStatus = Status.SUB_ACTIVITY;
1027 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
1028 } else {
Gary Mai3107b252016-11-02 18:26:07 -07001029 // Make sure the default account is automatically set if there is only one non-device
1030 // account.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001031 mEditorUtils.maybeUpdateDefaultAccount(accounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001032 // Otherwise, there should be a default account. Then either create a local contact
1033 // (if default account is null) or create a contact with the specified account.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001034 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(accounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001035 createContact(defaultAccount);
1036 }
1037 }
1038
1039 /**
Walter Jang7b0970f2016-09-01 10:40:19 -07001040 * Shows account creation screen associated with a given account.
1041 *
1042 * @param account may be null to signal a device-local contact should be created.
1043 */
1044 private void createContact(AccountWithDataSet account) {
1045 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1046 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1047
Gary Maiaebf3202016-09-22 18:11:15 -07001048 setStateForNewContact(account, accountType, isEditingUserProfile());
Walter Jang7b0970f2016-09-01 10:40:19 -07001049 }
1050
1051 //
1052 // Data binding
1053 //
1054
1055 private void setState(Contact contact) {
1056 // If we have already loaded data, we do not want to change it here to not confuse the user
1057 if (!mState.isEmpty()) {
Wenyi Wang57a0e982017-03-24 16:02:44 -07001058 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1059 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1060 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001061 return;
1062 }
Gary Mai7b751452016-11-07 17:04:04 -08001063 mContact = contact;
Gary Mai4ceabed2016-09-16 12:14:13 -07001064 mRawContacts = contact.getRawContacts();
Walter Jang7b0970f2016-09-01 10:40:19 -07001065
Walter Jang7b0970f2016-09-01 10:40:19 -07001066 // Check for writable raw contacts. If there are none, then we need to create one so user
1067 // can edit. For the user profile case, there is already an editable contact.
1068 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1069 mHasNewContact = true;
Gary Mai698cee72016-09-19 16:09:54 -07001070 mReadOnlyDisplayNameId = contact.getNameRawContactId();
1071 mCopyReadOnlyName = true;
Walter Jang7b0970f2016-09-01 10:40:19 -07001072 // This is potentially an asynchronous call and will add deltas to list.
1073 selectAccountAndCreateContact();
Walter Jang7b0970f2016-09-01 10:40:19 -07001074 } else {
1075 mHasNewContact = false;
1076 }
1077
Gary Mai698cee72016-09-19 16:09:54 -07001078 setStateForExistingContact(contact.isUserProfile(), mRawContacts);
Gary Maie4874662016-09-26 11:42:54 -07001079 if (mAutoAddToDefaultGroup
1080 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1081 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1082 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001083 }
1084
1085 /**
1086 * Prepare {@link #mState} for a newly created phone-local contact.
1087 */
1088 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1089 boolean isUserProfile) {
1090 setStateForNewContact(account, accountType, /* oldState =*/ null,
1091 /* oldAccountType =*/ null, isUserProfile);
1092 }
1093
1094 /**
1095 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1096 * specified by oldState and oldAccountType.
1097 */
1098 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1099 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1100 mStatus = Status.EDITING;
Gary Mai9f691e82017-01-10 16:47:45 -08001101 mAccountWithDataSet = account;
Walter Jang7b0970f2016-09-01 10:40:19 -07001102 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1103 mIsUserProfile = isUserProfile;
1104 mNewContactDataReady = true;
1105 bindEditors();
1106 }
1107
1108 /**
1109 * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1110 * {@link #mState}.
1111 *
1112 * If oldState and oldAccountType are specified, the state specified by those parameters
1113 * is migrated to the result {@link RawContactDelta}.
1114 */
1115 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1116 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1117 final RawContact rawContact = new RawContact();
1118 if (account != null) {
1119 rawContact.setAccount(account);
1120 } else {
1121 rawContact.setAccountToLocal();
1122 }
1123
1124 final RawContactDelta result = new RawContactDelta(
1125 ValuesDelta.fromAfter(rawContact.getValues()));
1126 if (oldState == null) {
1127 // Parse any values from incoming intent
1128 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1129 } else {
1130 RawContactModifier.migrateStateForNewContact(
1131 mContext, oldState, result, oldAccountType, accountType);
1132 }
1133
1134 // Ensure we have some default fields (if the account type does not support a field,
1135 // ensureKind will not add it, so it is safe to add e.g. Event)
Gary Mai62ec0b12016-10-07 14:23:54 -07001136 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001137 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1138 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1139 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1140 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1141 RawContactModifier.ensureKindExists(result, accountType,
1142 StructuredPostal.CONTENT_ITEM_TYPE);
1143
1144 // Set the correct URI for saving the contact as a profile
1145 if (mNewLocalProfile) {
1146 result.setProfileQueryUri();
1147 }
1148
1149 return result;
1150 }
1151
1152 /**
1153 * Prepare {@link #mState} for an existing contact.
1154 */
Gary Mai698cee72016-09-19 16:09:54 -07001155 private void setStateForExistingContact(boolean isUserProfile,
Walter Jang7b0970f2016-09-01 10:40:19 -07001156 ImmutableList<RawContact> rawContacts) {
1157 setEnabled(true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001158
1159 mState.addAll(rawContacts.iterator());
1160 setIntentExtras(mIntentExtras);
1161 mIntentExtras = null;
1162
1163 // For user profile, change the contacts query URI
1164 mIsUserProfile = isUserProfile;
1165 boolean localProfileExists = false;
1166
1167 if (mIsUserProfile) {
1168 for (RawContactDelta rawContactDelta : mState) {
1169 // For profile contacts, we need a different query URI
1170 rawContactDelta.setProfileQueryUri();
1171 // Try to find a local profile contact
1172 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1173 localProfileExists = true;
1174 }
1175 }
1176 // Editor should always present a local profile for editing
1177 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
1178 // going to prune all but the one raw contact that we're trying to display by itself.
1179 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1180 mState.add(createLocalRawContactDelta());
1181 }
1182 }
1183 mExistingContactDataReady = true;
1184 bindEditors();
1185 }
1186
1187 /**
1188 * Set the enabled state of editors.
1189 */
1190 private void setEnabled(boolean enabled) {
1191 if (mEnabled != enabled) {
1192 mEnabled = enabled;
1193
1194 // Enable/disable editors
1195 if (mContent != null) {
1196 int count = mContent.getChildCount();
1197 for (int i = 0; i < count; i++) {
1198 mContent.getChildAt(i).setEnabled(enabled);
1199 }
1200 }
1201
Walter Jang7b0970f2016-09-01 10:40:19 -07001202 // Maybe invalidate the options menu
1203 final Activity activity = getActivity();
1204 if (activity != null) activity.invalidateOptionsMenu();
1205 }
1206 }
1207
1208 /**
1209 * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1210 * {@link #mState}.
1211 */
1212 private static RawContactDelta createLocalRawContactDelta() {
1213 final RawContact rawContact = new RawContact();
1214 rawContact.setAccountToLocal();
1215
1216 final RawContactDelta result = new RawContactDelta(
1217 ValuesDelta.fromAfter(rawContact.getValues()));
1218 result.setProfileQueryUri();
1219
1220 return result;
1221 }
1222
Gary Mai698cee72016-09-19 16:09:54 -07001223 private void copyReadOnlyName() {
1224 // We should only ever be doing this if we're creating a new writable contact to attach to
1225 // a read only contact.
1226 if (!isEditingReadOnlyRawContactWithNewContact()) {
1227 return;
1228 }
1229 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1230 final RawContactDelta writable = mState.get(writableIndex);
Gary Mai7b751452016-11-07 17:04:04 -08001231 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
Gary Mai698cee72016-09-19 16:09:54 -07001232 final ValuesDelta writeNameDelta = writable
1233 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1234 final ValuesDelta readNameDelta = readOnly
1235 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
Gary Mai698cee72016-09-19 16:09:54 -07001236 mCopyReadOnlyName = false;
Gary Mai860698b2016-12-19 11:44:57 -08001237 if (writeNameDelta == null || readNameDelta == null) {
1238 return;
1239 }
1240 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
Gary Mai698cee72016-09-19 16:09:54 -07001241 }
1242
Walter Jang7b0970f2016-09-01 10:40:19 -07001243 /**
1244 * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1245 * Contact.
1246 */
Walter Jangba59deb2015-01-26 11:23:48 -08001247 protected void bindEditors() {
Walter Jangcab3dce2015-02-09 17:48:03 -08001248 if (!isReadyToBindEditors()) {
1249 return;
1250 }
1251
Walter Jangd35e5ef2015-02-24 09:18:16 -08001252 // Add input fields for the loaded Contact
Gary Mai363af602016-09-28 10:01:23 -07001253 final RawContactEditorView editorView = getContent();
Walter Jangb6ca2722015-02-20 11:10:25 -08001254 editorView.setListener(this);
Gary Mai698cee72016-09-19 16:09:54 -07001255 if (mCopyReadOnlyName) {
1256 copyReadOnlyName();
1257 }
Gary Mai678108e2016-10-26 14:34:33 -07001258 editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
Walter Jang9a552372016-08-24 11:51:05 -07001259 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
Gary Mai5a00de32016-10-19 18:20:41 -07001260 mRawContactIdToDisplayAlone);
Gary Mai079598f2016-11-03 15:02:45 -07001261 if (isEditingReadOnlyRawContact()) {
Gary Mai15646ce2016-11-17 10:54:01 -08001262 final Toolbar toolbar = getEditorActivity().getToolbar();
1263 if (toolbar != null) {
1264 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
Gary Maid8f3da62016-11-18 11:47:20 -08001265 // Set activity title for Talkback
1266 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
John Shaobd9ef3c2016-12-15 12:42:03 -08001267 toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
Gary Mai15646ce2016-11-17 10:54:01 -08001268 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
John Shaobd9ef3c2016-12-15 12:42:03 -08001269 toolbar.getNavigationIcon().setAutoMirrored(true);
Gary Mai079598f2016-11-03 15:02:45 -07001270 }
1271 }
Walter Jangcab3dce2015-02-09 17:48:03 -08001272
Walter Jangd35e5ef2015-02-24 09:18:16 -08001273 // Set up the photo widget
Walter Jang31a74ad2015-10-02 19:17:39 -07001274 editorView.setPhotoListener(this);
Walter Jang3efae4a2015-02-18 11:12:00 -08001275 mPhotoRawContactId = editorView.getPhotoRawContactId();
Walter Jang31a74ad2015-10-02 19:17:39 -07001276 // If there is an updated full resolution photo apply it now, this will be the case if
1277 // the user selects or takes a new photo, then rotates the device.
1278 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1279 if (uri != null) {
1280 editorView.setFullSizePhoto(uri);
Walter Jang41b3ea12015-03-09 17:30:06 -07001281 }
yoichi kakimoto5ed462a2017-09-04 19:20:13 +09001282 final StructuredNameEditorView nameEditor = editorView.getNameEditorView();
1283 final TextFieldsEditorView phoneticNameEditor = editorView.getPhoneticEditorView();
1284 final boolean useJapaneseOrder =
1285 Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
1286 if (useJapaneseOrder && nameEditor != null && phoneticNameEditor != null) {
1287 nameEditor.setPhoneticView(phoneticNameEditor);
1288 }
Walter Jang3efae4a2015-02-18 11:12:00 -08001289
Walter Jangd35e5ef2015-02-24 09:18:16 -08001290 // The editor is ready now so make it visible
Gary Mai678108e2016-10-26 14:34:33 -07001291 editorView.setEnabled(mEnabled);
Walter Jangd35e5ef2015-02-24 09:18:16 -08001292 editorView.setVisibility(View.VISIBLE);
1293
1294 // Refresh the ActionBar as the visibility of the join command
1295 // Activity can be null if we have been detached from the Activity.
Walter Jangcab3dce2015-02-09 17:48:03 -08001296 invalidateOptionsMenu();
1297 }
1298
Walter Jang7b0970f2016-09-01 10:40:19 -07001299 /**
1300 * Invalidates the options menu if we are still associated with an Activity.
1301 */
1302 private void invalidateOptionsMenu() {
1303 final Activity activity = getActivity();
1304 if (activity != null) {
1305 activity.invalidateOptionsMenu();
1306 }
1307 }
1308
Walter Jangcab3dce2015-02-09 17:48:03 -08001309 private boolean isReadyToBindEditors() {
1310 if (mState.isEmpty()) {
1311 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1312 Log.v(TAG, "No data to bind editors");
1313 }
1314 return false;
1315 }
1316 if (mIsEdit && !mExistingContactDataReady) {
1317 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1318 Log.v(TAG, "Existing contact data is not ready to bind editors.");
1319 }
1320 return false;
1321 }
1322 if (mHasNewContact && !mNewContactDataReady) {
1323 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1324 Log.v(TAG, "New contact data is not ready to bind editors.");
1325 }
1326 return false;
1327 }
Gary Mai1e899dc2017-02-07 15:08:53 -08001328 // Don't attempt to bind anything if we have no permissions.
1329 return RequestPermissionsActivity.hasRequiredPermissions(mContext);
Walter Jangba59deb2015-01-26 11:23:48 -08001330 }
1331
Walter Jang7b0970f2016-09-01 10:40:19 -07001332 /**
1333 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1334 * Some of old data are reused with new restriction enforced by the new account.
1335 *
1336 * @param oldState Old data being edited.
1337 * @param oldAccount Old account associated with oldState.
1338 * @param newAccount New account to be used.
1339 */
1340 private void rebindEditorsForNewContact(
1341 RawContactDelta oldState, AccountWithDataSet oldAccount,
1342 AccountWithDataSet newAccount) {
1343 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1344 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1345 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1346
Gary Maiaebf3202016-09-22 18:11:15 -07001347 mExistingContactDataReady = false;
1348 mNewContactDataReady = false;
1349 mState = new RawContactDeltaList();
1350 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1351 isEditingUserProfile());
1352 if (mIsEdit) {
Gary Mai698cee72016-09-19 16:09:54 -07001353 setStateForExistingContact(isEditingUserProfile(), mRawContacts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001354 }
1355 }
1356
1357 //
1358 // ContactEditor
1359 //
1360
Walter Jang3f990ba2015-01-27 17:38:30 +00001361 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -07001362 public void setListener(Listener listener) {
1363 mListener = listener;
1364 }
1365
1366 @Override
1367 public void load(String action, Uri lookupUri, Bundle intentExtras) {
1368 mAction = action;
1369 mLookupUri = lookupUri;
1370 mIntentExtras = intentExtras;
1371
1372 if (mIntentExtras != null) {
1373 mAutoAddToDefaultGroup =
1374 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1375 mNewLocalProfile =
1376 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1377 mDisableDeleteMenuOption =
1378 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1379 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1380 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1381 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1382 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1383 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1384 }
Gary Maia6c80b32016-09-30 16:34:55 -07001385 mRawContactIdToDisplayAlone = mIntentExtras
1386 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001387 }
1388 }
1389
1390 @Override
1391 public void setIntentExtras(Bundle extras) {
Gary Mai5336e6e2016-10-23 14:17:03 -07001392 getContent().setIntentExtras(extras);
Walter Jang7b0970f2016-09-01 10:40:19 -07001393 }
1394
1395 @Override
1396 public void onJoinCompleted(Uri uri) {
1397 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1398 }
1399
James Laskeye5a140a2016-10-18 15:43:42 -07001400
1401 private String getNameToDisplay(Uri contactUri) {
Gary Maic000d2e2016-11-18 13:51:17 -08001402 // The contact has been deleted or the uri is otherwise no longer right.
1403 if (contactUri == null) {
1404 return null;
1405 }
James Laskeye5a140a2016-10-18 15:43:42 -07001406 final ContentResolver resolver = mContext.getContentResolver();
1407 final Cursor cursor = resolver.query(contactUri, new String[]{
1408 ContactsContract.Contacts.DISPLAY_NAME,
1409 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
James Laskeye5a140a2016-10-18 15:43:42 -07001410
Gary Maia4adae12016-10-23 13:47:17 -07001411 if (cursor != null) {
1412 try {
1413 if (cursor.moveToFirst()) {
1414 final String displayName = cursor.getString(0);
1415 final String displayNameAlt = cursor.getString(1);
1416 cursor.close();
1417 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1418 new ContactsPreferences(mContext));
1419 }
1420 } finally {
1421 cursor.close();
1422 }
1423 }
James Laskeye5a140a2016-10-18 15:43:42 -07001424 return null;
1425 }
1426
1427
Walter Jang7b0970f2016-09-01 10:40:19 -07001428 @Override
1429 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1430 Uri contactLookupUri, Long joinContactId) {
1431 if (hadChanges) {
1432 if (saveSucceeded) {
1433 switch (saveMode) {
1434 case SaveMode.JOIN:
1435 break;
1436 case SaveMode.SPLIT:
1437 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1438 .show();
1439 break;
1440 default:
James Laskeye5a140a2016-10-18 15:43:42 -07001441 final String displayName = getNameToDisplay(contactLookupUri);
James Laskeyb1671052016-09-16 13:57:21 -07001442 final String toastMessage;
1443 if (!TextUtils.isEmpty(displayName)) {
1444 toastMessage = getResources().getString(
1445 R.string.contactSavedNamedToast, displayName);
1446 } else {
1447 toastMessage = getResources().getString(R.string.contactSavedToast);
1448 }
1449 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
Walter Jang7b0970f2016-09-01 10:40:19 -07001450 }
1451
1452 } else {
1453 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1454 }
1455 }
1456 switch (saveMode) {
1457 case SaveMode.CLOSE: {
Walter Jang8d26c0a2017-02-08 10:56:04 -08001458 final Intent resultIntent;
Walter Jang7b0970f2016-09-01 10:40:19 -07001459 if (saveSucceeded && contactLookupUri != null) {
1460 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1461 mContext, contactLookupUri, mLookupUri);
Walter Jang8d26c0a2017-02-08 10:56:04 -08001462 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1463 mContext, lookupUri, ScreenType.EDITOR);
1464 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001465 } else {
1466 resultIntent = null;
1467 }
1468 // It is already saved, so prevent it from being saved again
1469 mStatus = Status.CLOSING;
1470 if (mListener != null) mListener.onSaveFinished(resultIntent);
1471 break;
1472 }
Gary Mai363af602016-09-28 10:01:23 -07001473 case SaveMode.EDITOR: {
Walter Jang7b0970f2016-09-01 10:40:19 -07001474 // It is already saved, so prevent it from being saved again
1475 mStatus = Status.CLOSING;
1476 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1477 break;
1478 }
1479 case SaveMode.JOIN:
1480 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1481 joinAggregate(joinContactId);
1482 }
1483 break;
1484 case SaveMode.RELOAD:
1485 if (saveSucceeded && contactLookupUri != null) {
1486 // If this was in INSERT, we are changing into an EDIT now.
1487 // If it already was an EDIT, we are changing to the new Uri now
1488 mState = new RawContactDeltaList();
1489 load(Intent.ACTION_EDIT, contactLookupUri, null);
1490 mStatus = Status.LOADING;
1491 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1492 }
1493 break;
1494
1495 case SaveMode.SPLIT:
1496 mStatus = Status.CLOSING;
1497 if (mListener != null) {
1498 mListener.onContactSplit(contactLookupUri);
Wenyi Wang57a0e982017-03-24 16:02:44 -07001499 } else if (Log.isLoggable(TAG, Log.DEBUG)) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001500 Log.d(TAG, "No listener registered, can not call onSplitFinished");
1501 }
1502 break;
1503 }
1504 }
1505
1506 /**
1507 * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1508 *
1509 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1510 */
1511 private void showJoinAggregateActivity(Uri contactLookupUri) {
1512 if (contactLookupUri == null || !isAdded()) {
1513 return;
1514 }
1515
1516 mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1517 final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1518 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1519 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1520 startActivityForResult(intent, REQUEST_CODE_JOIN);
1521 }
1522
1523 //
1524 // Aggregation PopupWindow
1525 //
1526
1527 /**
1528 * Triggers an asynchronous search for aggregation suggestions.
1529 */
1530 protected void acquireAggregationSuggestions(Context context,
1531 long rawContactId, ValuesDelta valuesDelta) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001532 mAggregationSuggestionsRawContactId = rawContactId;
1533
1534 if (mAggregationSuggestionEngine == null) {
1535 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1536 mAggregationSuggestionEngine.setListener(this);
1537 mAggregationSuggestionEngine.start();
1538 }
1539
1540 mAggregationSuggestionEngine.setContactId(getContactId());
Gary Mai220d10c2016-09-23 13:56:39 -07001541 mAggregationSuggestionEngine.setAccountFilter(
1542 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
Walter Jang7b0970f2016-09-01 10:40:19 -07001543
1544 mAggregationSuggestionEngine.onNameChange(valuesDelta);
1545 }
1546
1547 /**
1548 * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1549 */
1550 private long getContactId() {
1551 for (RawContactDelta rawContact : mState) {
1552 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1553 if (contactId != null) {
1554 return contactId;
1555 }
1556 }
1557 return 0;
1558 }
1559
1560 @Override
1561 public void onAggregationSuggestionChange() {
1562 final Activity activity = getActivity();
1563 if ((activity != null && activity.isFinishing())
1564 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
1565 return;
1566 }
1567
1568 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1569
1570 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1571 return;
1572 }
1573
Gary Maida20b472016-09-20 14:46:40 -07001574 final View anchorView = getAggregationAnchorView();
Walter Jang7b0970f2016-09-01 10:40:19 -07001575 if (anchorView == null) {
1576 return; // Raw contact deleted?
1577 }
1578 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1579 mAggregationSuggestionPopup.setAnchorView(anchorView);
1580 mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1581 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1582 mAggregationSuggestionPopup.setAdapter(
1583 new AggregationSuggestionAdapter(
1584 getActivity(),
Walter Jang7b0970f2016-09-01 10:40:19 -07001585 /* listener =*/ this,
1586 mAggregationSuggestionEngine.getSuggestions()));
1587 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1588 @Override
1589 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1590 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1591 suggestionView.handleItemClickEvent();
1592 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1593 mAggregationSuggestionPopup = null;
1594 }
1595 });
1596 mAggregationSuggestionPopup.show();
1597 }
1598
1599 /**
Gary Maida20b472016-09-20 14:46:40 -07001600 * Returns the editor view that should be used as the anchor for aggregation suggestions.
Walter Jang7b0970f2016-09-01 10:40:19 -07001601 */
Gary Maida20b472016-09-20 14:46:40 -07001602 protected View getAggregationAnchorView() {
Walter Jangd35e5ef2015-02-24 09:18:16 -08001603 return getContent().getAggregationAnchorView();
1604 }
1605
Walter Jang7b0970f2016-09-01 10:40:19 -07001606 /**
1607 * Joins the suggested contact (specified by the id's of constituent raw
1608 * contacts), save all changes, and stay in the editor.
1609 */
1610 public void doJoinSuggestedContact(long[] rawContactIds) {
1611 if (!hasValidState() || mStatus != Status.EDITING) {
1612 return;
1613 }
1614
1615 mState.setJoinWithRawContacts(rawContactIds);
1616 save(SaveMode.RELOAD);
1617 }
1618
1619 @Override
Gary Mai678108e2016-10-26 14:34:33 -07001620 public void onEditAction(Uri contactLookupUri, long rawContactId) {
1621 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
Walter Jang7b0970f2016-09-01 10:40:19 -07001622 }
1623
1624 /**
Gary Mai678108e2016-10-26 14:34:33 -07001625 * Abandons the currently edited contact and switches to editing the selected raw contact,
1626 * transferring all the data there
Walter Jang7b0970f2016-09-01 10:40:19 -07001627 */
Gary Mai678108e2016-10-26 14:34:33 -07001628 public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001629 if (mListener != null) {
1630 // make sure we don't save this contact when closing down
1631 mStatus = Status.CLOSING;
Gary Mai678108e2016-10-26 14:34:33 -07001632 mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1633 getContent().getCurrentRawContactDelta().getContentValues());
Walter Jang7b0970f2016-09-01 10:40:19 -07001634 }
1635 }
1636
1637 /**
1638 * Sets group metadata on all bound editors.
1639 */
Walter Jang92f8ccc2015-02-06 10:23:37 -08001640 protected void setGroupMetaData() {
Walter Jangf10ca152015-09-22 15:23:55 -07001641 if (mGroupMetaData != null) {
1642 getContent().setGroupMetaData(mGroupMetaData);
1643 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001644 }
1645
Walter Jang7b0970f2016-09-01 10:40:19 -07001646 /**
1647 * Persist the accumulated editor deltas.
1648 *
1649 * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1650 * may be null.
1651 */
Walter Jange3373dc2015-10-27 15:35:12 -07001652 protected boolean doSaveAction(int saveMode, Long joinContactId) {
Walter Jang49ed2032015-02-11 20:09:05 -08001653 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1654 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1655 ((Activity) mContext).getClass(),
Gary Mai363af602016-09-28 10:01:23 -07001656 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
Walter Jange3373dc2015-10-27 15:35:12 -07001657 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
Wenyi Wangdd7d4562015-12-08 13:33:43 -08001658 return startSaveService(mContext, intent, saveMode);
Walter Jang49ed2032015-02-11 20:09:05 -08001659 }
1660
Walter Jang7b0970f2016-09-01 10:40:19 -07001661 private boolean startSaveService(Context context, Intent intent, int saveMode) {
1662 final boolean result = ContactSaveService.startService(
1663 context, intent, saveMode);
1664 if (!result) {
1665 onCancelEditConfirmed();
1666 }
1667 return result;
1668 }
1669
1670 //
1671 // Join Activity
1672 //
1673
1674 /**
1675 * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1676 */
Walter Jang49ed2032015-02-11 20:09:05 -08001677 protected void joinAggregate(final long contactId) {
1678 final Intent intent = ContactSaveService.createJoinContactsIntent(
Gary Mai363af602016-09-28 10:01:23 -07001679 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1680 ContactEditorActivity.ACTION_JOIN_COMPLETED);
Walter Jang49ed2032015-02-11 20:09:05 -08001681 mContext.startService(intent);
Walter Jang3f990ba2015-01-27 17:38:30 +00001682 }
Walter Jangb6ca2722015-02-20 11:10:25 -08001683
Walter Jang31a74ad2015-10-02 19:17:39 -07001684 public void removePhoto() {
1685 getContent().removePhoto();
1686 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
Walter Jang0e72ce92015-02-23 12:27:21 -08001687 }
1688
Walter Jang31a74ad2015-10-02 19:17:39 -07001689 public void updatePhoto(Uri uri) throws FileNotFoundException {
1690 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1691 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Wenyi Wang9bc9ba82015-11-17 19:37:33 -08001692 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Walter Jang31a74ad2015-10-02 19:17:39 -07001693 Toast.LENGTH_SHORT).show();
1694 return;
Walter Jang0e72ce92015-02-23 12:27:21 -08001695 }
Walter Jang31a74ad2015-10-02 19:17:39 -07001696 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1697 getContent().updatePhoto(uri);
Walter Jang0e72ce92015-02-23 12:27:21 -08001698 }
1699
Gary Maida20b472016-09-20 14:46:40 -07001700 public void setPrimaryPhoto() {
1701 getContent().setPrimaryPhoto();
Walter Jang0e72ce92015-02-23 12:27:21 -08001702 }
1703
1704 @Override
Walter Jang151f3e62015-02-26 15:29:40 -08001705 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1706 final Activity activity = getActivity();
1707 if (activity == null || activity.isFinishing()) {
1708 return;
1709 }
Walter Jang45b86d52015-10-15 15:23:16 -07001710 acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
Walter Jang151f3e62015-02-26 15:29:40 -08001711 }
1712
Walter Jang5a7a23b2015-03-06 10:54:26 -08001713 @Override
Walter Jang708ea9e2015-09-10 15:42:05 -07001714 public void onRebindEditorsForNewContact(RawContactDelta oldState,
1715 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1716 mNewContactAccountChanged = true;
Walter Jang708ea9e2015-09-10 15:42:05 -07001717 rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1718 }
1719
Walter Jang79658e12015-09-24 10:36:26 -07001720 @Override
1721 public void onBindEditorsFailed() {
1722 final Activity activity = getActivity();
1723 if (activity != null && !activity.isFinishing()) {
Gary Mai363af602016-09-28 10:01:23 -07001724 Toast.makeText(activity, R.string.editor_failed_to_load,
Walter Jang79658e12015-09-24 10:36:26 -07001725 Toast.LENGTH_SHORT).show();
1726 activity.setResult(Activity.RESULT_CANCELED);
1727 activity.finish();
1728 }
1729 }
1730
Walter Jangd6753152015-10-02 09:23:13 -07001731 @Override
1732 public void onEditorsBound() {
Wenyi Wang3cb77bb2016-07-27 17:39:03 -07001733 final Activity activity = getActivity();
1734 if (activity == null || activity.isFinishing()) {
1735 return;
1736 }
Walter Jangd6753152015-10-02 09:23:13 -07001737 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1738 }
1739
Walter Jang31a74ad2015-10-02 19:17:39 -07001740 @Override
1741 public void onPhotoEditorViewClicked() {
Walter Jang3f18d612015-10-07 16:01:05 -07001742 // For contacts composed of a single writable raw contact, or raw contacts have no more
1743 // than 1 photo, clicking the photo view simply opens the source photo dialog
Walter Jang31a74ad2015-10-02 19:17:39 -07001744 getEditorActivity().changePhoto(getPhotoMode());
1745 }
1746
1747 private int getPhotoMode() {
Gary Maida20b472016-09-20 14:46:40 -07001748 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1749 : PhotoActionPopup.Modes.NO_PHOTO;
Walter Jang31a74ad2015-10-02 19:17:39 -07001750 }
1751
Gary Mai363af602016-09-28 10:01:23 -07001752 private ContactEditorActivity getEditorActivity() {
1753 return (ContactEditorActivity) getActivity();
Walter Jang31a74ad2015-10-02 19:17:39 -07001754 }
1755
Gary Mai363af602016-09-28 10:01:23 -07001756 private RawContactEditorView getContent() {
1757 return (RawContactEditorView) mContent;
Walter Jang3efae4a2015-02-18 11:12:00 -08001758 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001759}