blob: 4cef614337bd3b5e74e73d9522120102a71c6e22 [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;
Marcus Hagerott98388512018-03-30 09:22:13 -070034import android.os.Handler;
Walter Jang7b0970f2016-09-01 10:40:19 -070035import android.os.SystemClock;
36import android.provider.ContactsContract;
37import android.provider.ContactsContract.CommonDataKinds.Email;
38import android.provider.ContactsContract.CommonDataKinds.Event;
39import android.provider.ContactsContract.CommonDataKinds.Organization;
40import android.provider.ContactsContract.CommonDataKinds.Phone;
41import android.provider.ContactsContract.CommonDataKinds.StructuredName;
42import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
43import android.provider.ContactsContract.Intents;
44import android.provider.ContactsContract.RawContacts;
Aravind Sreekumar71212852018-04-06 15:47:45 -070045import androidx.appcompat.widget.Toolbar;
Walter Jange3945952015-10-27 12:44:54 -070046import android.text.TextUtils;
Walter Jangcab3dce2015-02-09 17:48:03 -080047import android.util.Log;
Walter Jang3f990ba2015-01-27 17:38:30 +000048import android.view.LayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -070049import android.view.Menu;
50import android.view.MenuInflater;
Walter Jangc90cc152015-06-19 14:15:08 -070051import android.view.MenuItem;
Walter Jang3f990ba2015-01-27 17:38:30 +000052import android.view.View;
53import android.view.ViewGroup;
Marcus Hagerott98388512018-03-30 09:22:13 -070054import android.view.inputmethod.InputMethodManager;
Walter Jang7b0970f2016-09-01 10:40:19 -070055import android.widget.AdapterView;
56import android.widget.BaseAdapter;
yoichi kakimoto5ed462a2017-09-04 19:20:13 +090057import android.widget.EditText;
Walter Jang3f990ba2015-01-27 17:38:30 +000058import android.widget.LinearLayout;
Walter Jang7b0970f2016-09-01 10:40:19 -070059import android.widget.ListPopupWindow;
Walter Jang79658e12015-09-24 10:36:26 -070060import android.widget.Toast;
Walter Jang3f990ba2015-01-27 17:38:30 +000061
Walter Jang7b0970f2016-09-01 10:40:19 -070062import com.android.contacts.ContactSaveService;
63import com.android.contacts.GroupMetaDataLoader;
64import com.android.contacts.R;
Gary Maia4adae12016-10-23 13:47:17 -070065import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
Gary Mai363af602016-09-28 10:01:23 -070066import com.android.contacts.activities.ContactEditorActivity;
67import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
Walter Jang7b0970f2016-09-01 10:40:19 -070068import com.android.contacts.activities.ContactSelectionActivity;
Gary Mai1e899dc2017-02-07 15:08:53 -080069import com.android.contacts.activities.RequestPermissionsActivity;
Gary Mai0a49afa2016-12-05 15:53:58 -080070import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
71import com.android.contacts.group.GroupUtil;
72import com.android.contacts.list.UiIntentActions;
Gary Mai69c182a2016-12-05 13:07:03 -080073import com.android.contacts.logging.ScreenEvent.ScreenType;
74import com.android.contacts.model.AccountTypeManager;
75import com.android.contacts.model.Contact;
76import com.android.contacts.model.ContactLoader;
77import com.android.contacts.model.RawContact;
78import com.android.contacts.model.RawContactDelta;
79import com.android.contacts.model.RawContactDeltaList;
80import com.android.contacts.model.RawContactModifier;
81import com.android.contacts.model.ValuesDelta;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -080082import com.android.contacts.model.account.AccountInfo;
Gary Mai69c182a2016-12-05 13:07:03 -080083import com.android.contacts.model.account.AccountType;
84import com.android.contacts.model.account.AccountWithDataSet;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -080085import com.android.contacts.model.account.AccountsLoader;
Gary Mai69c182a2016-12-05 13:07:03 -080086import com.android.contacts.preference.ContactsPreferences;
Gary Maie4874662016-09-26 11:42:54 -070087import com.android.contacts.quickcontact.InvisibleContactUtil;
Walter Jang7b0970f2016-09-01 10:40:19 -070088import com.android.contacts.quickcontact.QuickContactActivity;
Gary Mai0a49afa2016-12-05 15:53:58 -080089import com.android.contacts.util.ContactDisplayUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070090import com.android.contacts.util.ContactPhotoUtils;
Gary Mai0a49afa2016-12-05 15:53:58 -080091import com.android.contacts.util.ImplicitIntentsUtil;
92import com.android.contacts.util.MaterialColorMapUtils;
Walter Jang7b0970f2016-09-01 10:40:19 -070093import com.android.contacts.util.UiClosables;
Gary Maia4adae12016-10-23 13:47:17 -070094import com.android.contactsbind.HelpUtils;
Walter Jang8d26c0a2017-02-08 10:56:04 -080095
Marcus Hagerott807e6202016-12-21 08:59:50 -080096import com.google.common.base.Preconditions;
Walter Jang7b0970f2016-09-01 10:40:19 -070097import com.google.common.collect.ImmutableList;
98import com.google.common.collect.Lists;
99
Walter Jang3efae4a2015-02-18 11:12:00 -0800100import java.io.FileNotFoundException;
Walter Jang31a74ad2015-10-02 19:17:39 -0700101import java.util.ArrayList;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800102import java.util.Collections;
Walter Jang7b0970f2016-09-01 10:40:19 -0700103import java.util.HashSet;
104import java.util.Iterator;
105import java.util.List;
yoichi kakimoto5ed462a2017-09-04 19:20:13 +0900106import java.util.Locale;
Walter Jang7b0970f2016-09-01 10:40:19 -0700107import java.util.Set;
Marcus Hagerott98388512018-03-30 09:22:13 -0700108import javax.annotation.Nullable;
Walter Jang3efae4a2015-02-18 11:12:00 -0800109
Walter Jang3f990ba2015-01-27 17:38:30 +0000110/**
111 * Contact editor with only the most important fields displayed initially.
112 */
Gary Mai363af602016-09-28 10:01:23 -0700113public class ContactEditorFragment extends Fragment implements
Walter Jang7b0970f2016-09-01 10:40:19 -0700114 ContactEditor, SplitContactConfirmationDialogFragment.Listener,
115 JoinContactConfirmationDialogFragment.Listener,
116 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
117 CancelEditDialogFragment.Listener,
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800118 RawContactEditorView.Listener, PhotoEditorView.Listener,
119 AccountsLoader.AccountsListener {
Walter Jang3f990ba2015-01-27 17:38:30 +0000120
Walter Jang7b0970f2016-09-01 10:40:19 -0700121 static final String TAG = "ContactEditor";
122
123 private static final int LOADER_CONTACT = 1;
124 private static final int LOADER_GROUPS = 2;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800125 private static final int LOADER_ACCOUNTS = 3;
Walter Jang7b0970f2016-09-01 10:40:19 -0700126
Marcus Hagerott98388512018-03-30 09:22:13 -0700127 // How long to delay before attempting to restore focus and keyboard
128 // visibility after view state has been restored (e.g. after rotation)
129 // See b/77246197
130 private static final long RESTORE_FOCUS_DELAY_MILLIS = 100L;
131
Walter Jang3efae4a2015-02-18 11:12:00 -0800132 private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
Walter Jang28a27272015-09-19 16:06:08 -0700133 private static final String KEY_UPDATED_PHOTOS = "updated_photos";
Walter Jang3efae4a2015-02-18 11:12:00 -0800134
Walter Jang7b0970f2016-09-01 10:40:19 -0700135 private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
136 add(Intent.ACTION_EDIT);
137 add(Intent.ACTION_INSERT);
Gary Mai363af602016-09-28 10:01:23 -0700138 add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
Walter Jang7b0970f2016-09-01 10:40:19 -0700139 }};
140
141 private static final String KEY_ACTION = "action";
142 private static final String KEY_URI = "uri";
143 private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
144 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
145 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
146 private static final String KEY_MATERIAL_PALETTE = "materialPalette";
Gary Maic135a5d2016-12-19 11:13:46 -0800147 private static final String KEY_ACCOUNT = "saveToAccount";
Walter Jang7b0970f2016-09-01 10:40:19 -0700148 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
149
150 private static final String KEY_RAW_CONTACTS = "rawContacts";
151
152 private static final String KEY_EDIT_STATE = "state";
153 private static final String KEY_STATUS = "status";
154
155 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
156 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
157
158 private static final String KEY_IS_EDIT = "isEdit";
159 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
160
Walter Jang7b0970f2016-09-01 10:40:19 -0700161 private static final String KEY_IS_USER_PROFILE = "isUserProfile";
162
163 private static final String KEY_ENABLED = "enabled";
164
165 // Aggregation PopupWindow
166 private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
167 "aggregationSuggestionsRawContactId";
168
169 // Join Activity
170 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
171
Gary Mai698cee72016-09-19 16:09:54 -0700172 private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
173 private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
Walter Jang7b0970f2016-09-01 10:40:19 -0700174
Marcus Hagerott98388512018-03-30 09:22:13 -0700175 private static final String KEY_FOCUSED_VIEW_ID = "focusedViewId";
176
177 private static final String KEY_RESTORE_SOFT_INPUT = "restoreSoftInput";
178
Walter Jang7b0970f2016-09-01 10:40:19 -0700179 protected static final int REQUEST_CODE_JOIN = 0;
180 protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700181
Walter Jang7b0970f2016-09-01 10:40:19 -0700182 /**
183 * An intent extra that forces the editor to add the edited contact
184 * to the default group (e.g. "My Contacts").
185 */
186 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
187
188 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
189
190 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
191 "disableDeleteMenuOption";
192
193 /**
194 * Intent key to pass the photo palette primary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700195 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700196 */
197 public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
198 "material_palette_primary_color";
199
200 /**
201 * Intent key to pass the photo palette secondary color calculated by
Gary Mai363af602016-09-28 10:01:23 -0700202 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
Walter Jang7b0970f2016-09-01 10:40:19 -0700203 */
204 public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
205 "material_palette_secondary_color";
206
207 /**
208 * Intent key to pass the ID of the photo to display on the editor.
209 */
Gary Maida20b472016-09-20 14:46:40 -0700210 // TODO: This can be cleaned up if we decide to not pass the photo id through
211 // QuickContactActivity.
Walter Jang7b0970f2016-09-01 10:40:19 -0700212 public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
213
214 /**
Gary Maia6c80b32016-09-30 16:34:55 -0700215 * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
216 * by itself.
217 */
218 public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
219 "raw_contact_id_to_display_alone";
220
221 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700222 * Intent extra to specify a {@link ContactEditor.SaveMode}.
223 */
224 public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
225
226 /**
227 * Intent extra key for the contact ID to join the current contact to after saving.
228 */
229 public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
230
231 /**
232 * Callbacks for Activities that host contact editors Fragments.
233 */
234 public interface Listener {
235
236 /**
237 * Contact was not found, so somehow close this fragment. This is raised after a contact
238 * is removed via Menu/Delete
239 */
240 void onContactNotFound();
241
242 /**
243 * Contact was split, so we can close now.
244 *
245 * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
246 * The editor tries best to chose the most natural contact here.
247 */
248 void onContactSplit(Uri newLookupUri);
249
250 /**
251 * User has tapped Revert, close the fragment now.
252 */
253 void onReverted();
254
255 /**
256 * Contact was saved and the Fragment can now be closed safely.
257 */
258 void onSaveFinished(Intent resultIntent);
259
260 /**
Gary Mai678108e2016-10-26 14:34:33 -0700261 * User switched to editing a different raw contact (a suggestion from the
Walter Jang7b0970f2016-09-01 10:40:19 -0700262 * aggregation engine).
263 */
Gary Mai678108e2016-10-26 14:34:33 -0700264 void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
Walter Jang7b0970f2016-09-01 10:40:19 -0700265 ArrayList<ContentValues> contentValues);
266
267 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700268 * User has requested that contact be deleted.
269 */
270 void onDeleteRequested(Uri contactUri);
271 }
272
273 /**
274 * Adapter for aggregation suggestions displayed in a PopupWindow when
275 * editor fields change.
276 */
277 private static final class AggregationSuggestionAdapter extends BaseAdapter {
278 private final LayoutInflater mLayoutInflater;
Walter Jang7b0970f2016-09-01 10:40:19 -0700279 private final AggregationSuggestionView.Listener mListener;
280 private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
281
Gary Mai678108e2016-10-26 14:34:33 -0700282 public AggregationSuggestionAdapter(Activity activity,
Walter Jang7b0970f2016-09-01 10:40:19 -0700283 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
284 mLayoutInflater = activity.getLayoutInflater();
Walter Jang7b0970f2016-09-01 10:40:19 -0700285 mListener = listener;
286 mSuggestions = suggestions;
287 }
288
289 @Override
290 public View getView(int position, View convertView, ViewGroup parent) {
291 final Suggestion suggestion = (Suggestion) getItem(position);
292 final AggregationSuggestionView suggestionView =
293 (AggregationSuggestionView) mLayoutInflater.inflate(
294 R.layout.aggregation_suggestions_item, null);
Walter Jang7b0970f2016-09-01 10:40:19 -0700295 suggestionView.setListener(mListener);
296 suggestionView.bindSuggestion(suggestion);
297 return suggestionView;
298 }
299
300 @Override
301 public long getItemId(int position) {
302 return position;
303 }
304
305 @Override
306 public Object getItem(int position) {
307 return mSuggestions.get(position);
308 }
309
310 @Override
311 public int getCount() {
312 return mSuggestions.size();
313 }
314 }
315
316 protected Context mContext;
317 protected Listener mListener;
318
319 //
320 // Views
321 //
322 protected LinearLayout mContent;
Walter Jang7b0970f2016-09-01 10:40:19 -0700323 protected ListPopupWindow mAggregationSuggestionPopup;
324
325 //
326 // Parameters passed in on {@link #load}
327 //
328 protected String mAction;
329 protected Uri mLookupUri;
330 protected Bundle mIntentExtras;
331 protected boolean mAutoAddToDefaultGroup;
332 protected boolean mDisableDeleteMenuOption;
333 protected boolean mNewLocalProfile;
334 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
Walter Jang7b0970f2016-09-01 10:40:19 -0700335
336 //
337 // Helpers
338 //
339 protected ContactEditorUtils mEditorUtils;
340 protected RawContactDeltaComparator mComparator;
341 protected ViewIdGenerator mViewIdGenerator;
342 private AggregationSuggestionEngine mAggregationSuggestionEngine;
343
344 //
345 // Loaded data
346 //
347 // Used to store existing contact data so it can be re-applied during a rebind call,
348 // i.e. account switch.
Gary Mai7b751452016-11-07 17:04:04 -0800349 protected Contact mContact;
Walter Jang7b0970f2016-09-01 10:40:19 -0700350 protected ImmutableList<RawContact> mRawContacts;
351 protected Cursor mGroupMetaData;
352
353 //
354 // Editor state
355 //
356 protected RawContactDeltaList mState;
357 protected int mStatus;
358 protected long mRawContactIdToDisplayAlone = -1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700359
360 // Whether to show the new contact blank form and if it's corresponding delta is ready.
361 protected boolean mHasNewContact;
362 protected AccountWithDataSet mAccountWithDataSet;
Marcus Hagerott4bd50d62016-12-15 15:52:22 -0800363 protected List<AccountInfo> mWritableAccounts = Collections.emptyList();
Walter Jang7b0970f2016-09-01 10:40:19 -0700364 protected boolean mNewContactDataReady;
365 protected boolean mNewContactAccountChanged;
366
367 // Whether it's an edit of existing contact and if it's corresponding delta is ready.
368 protected boolean mIsEdit;
369 protected boolean mExistingContactDataReady;
370
371 // Whether we are editing the "me" profile
372 protected boolean mIsUserProfile;
373
Walter Jang7b0970f2016-09-01 10:40:19 -0700374 // Whether editor views and options menu items should be enabled
375 private boolean mEnabled = true;
376
377 // Aggregation PopupWindow
378 private long mAggregationSuggestionsRawContactId;
379
380 // Join Activity
381 protected long mContactIdForJoin;
382
383 // 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 -0700384 protected long mReadOnlyDisplayNameId;
385 protected boolean mCopyReadOnlyName;
Walter Jang7b0970f2016-09-01 10:40:19 -0700386
387 /**
388 * The contact data loader listener.
389 */
390 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
391 new LoaderManager.LoaderCallbacks<Contact>() {
392
393 protected long mLoaderStartTime;
394
395 @Override
396 public Loader<Contact> onCreateLoader(int id, Bundle args) {
397 mLoaderStartTime = SystemClock.elapsedRealtime();
Gary Maie4874662016-09-26 11:42:54 -0700398 return new ContactLoader(mContext, mLookupUri,
399 /* postViewNotification */ true,
400 /* loadGroupMetaData */ true);
Walter Jang7b0970f2016-09-01 10:40:19 -0700401 }
402
403 @Override
404 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
405 final long loaderCurrentTime = SystemClock.elapsedRealtime();
Wenyi Wang57a0e982017-03-24 16:02:44 -0700406 if (Log.isLoggable(TAG, Log.VERBOSE)) {
407 Log.v(TAG,
408 "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
409 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700410 if (!contact.isLoaded()) {
411 // Item has been deleted. Close activity without saving again.
412 Log.i(TAG, "No contact found. Closing activity");
413 mStatus = Status.CLOSING;
414 if (mListener != null) mListener.onContactNotFound();
415 return;
416 }
417
418 mStatus = Status.EDITING;
419 mLookupUri = contact.getLookupUri();
420 final long setDataStartTime = SystemClock.elapsedRealtime();
421 setState(contact);
Walter Jang7b0970f2016-09-01 10:40:19 -0700422 final long setDataEndTime = SystemClock.elapsedRealtime();
Wenyi Wang57a0e982017-03-24 16:02:44 -0700423 if (Log.isLoggable(TAG, Log.VERBOSE)) {
424 Log.v(TAG, "Time needed for setting UI: "
425 + (setDataEndTime - setDataStartTime));
426 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700427 }
428
429 @Override
430 public void onLoaderReset(Loader<Contact> loader) {
431 }
432 };
433
434 /**
435 * The groups meta data loader listener.
436 */
437 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
438 new LoaderManager.LoaderCallbacks<Cursor>() {
439
440 @Override
441 public CursorLoader onCreateLoader(int id, Bundle args) {
Gary Mai5c1bff22016-09-30 15:10:25 -0700442 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
443 GroupUtil.ALL_GROUPS_SELECTION);
Walter Jang7b0970f2016-09-01 10:40:19 -0700444 }
445
446 @Override
447 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
448 mGroupMetaData = data;
449 setGroupMetaData();
450 }
451
452 @Override
453 public void onLoaderReset(Loader<Cursor> loader) {
454 }
455 };
456
Walter Jang3efae4a2015-02-18 11:12:00 -0800457 private long mPhotoRawContactId;
Walter Jang28a27272015-09-19 16:06:08 -0700458 private Bundle mUpdatedPhotos = new Bundle();
Walter Jang3efae4a2015-02-18 11:12:00 -0800459
Marcus Hagerott98388512018-03-30 09:22:13 -0700460 private InputMethodManager inputMethodManager;
461
Walter Jang3efae4a2015-02-18 11:12:00 -0800462 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700463 public Context getContext() {
464 return getActivity();
465 }
466
467 @Override
468 public void onAttach(Activity activity) {
469 super.onAttach(activity);
470 mContext = activity;
Marcus Hagerotta7978d52016-09-22 15:31:46 -0700471 mEditorUtils = ContactEditorUtils.create(mContext);
Walter Jang7b0970f2016-09-01 10:40:19 -0700472 mComparator = new RawContactDeltaComparator(mContext);
473 }
474
475 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800476 public void onCreate(Bundle savedState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700477 if (savedState != null) {
478 // Restore mUri before calling super.onCreate so that onInitializeLoaders
479 // would already have a uri and an action to work with
480 mAction = savedState.getString(KEY_ACTION);
481 mLookupUri = savedState.getParcelable(KEY_URI);
482 }
483
Walter Jang3efae4a2015-02-18 11:12:00 -0800484 super.onCreate(savedState);
485
Marcus Hagerott98388512018-03-30 09:22:13 -0700486 inputMethodManager =
487 (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
488
Walter Jang7b0970f2016-09-01 10:40:19 -0700489 if (savedState == null) {
490 mViewIdGenerator = new ViewIdGenerator();
491
492 // mState can still be null because it may not have have finished loading before
493 // onSaveInstanceState was called.
494 mState = new RawContactDeltaList();
495 } else {
496 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
497
498 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
499 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
500 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
501 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
Gary Maic135a5d2016-12-19 11:13:46 -0800502 mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
Walter Jang7b0970f2016-09-01 10:40:19 -0700503 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
504 KEY_RAW_CONTACTS));
505 // NOTE: mGroupMetaData is not saved/restored
506
507 // Read state from savedState. No loading involved here
508 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
509 mStatus = savedState.getInt(KEY_STATUS);
Walter Jang7b0970f2016-09-01 10:40:19 -0700510
511 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
512 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
513
514 mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
515 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
516
517 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
518
Walter Jang7b0970f2016-09-01 10:40:19 -0700519 mEnabled = savedState.getBoolean(KEY_ENABLED);
520
521 // Aggregation PopupWindow
522 mAggregationSuggestionsRawContactId = savedState.getLong(
523 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
524
525 // Join Activity
526 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
527
Gary Mai698cee72016-09-19 16:09:54 -0700528 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
529 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700530
Walter Jang3efae4a2015-02-18 11:12:00 -0800531 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
Walter Jang28a27272015-09-19 16:06:08 -0700532 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
Walter Jang3efae4a2015-02-18 11:12:00 -0800533 }
534 }
535
Walter Jang3f990ba2015-01-27 17:38:30 +0000536 @Override
Walter Jang3f990ba2015-01-27 17:38:30 +0000537 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Walter Janged8f6c92015-01-30 16:07:47 -0800538 setHasOptionsMenu(true);
539
Walter Jang3f990ba2015-01-27 17:38:30 +0000540 final View view = inflater.inflate(
Gary Mai363af602016-09-28 10:01:23 -0700541 R.layout.contact_editor_fragment, container, false);
Walter Jangf5dfea42015-09-16 12:30:36 -0700542 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
Walter Jang3f990ba2015-01-27 17:38:30 +0000543 return view;
544 }
545
Walter Janged8f6c92015-01-30 16:07:47 -0800546 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700547 public void onActivityCreated(Bundle savedInstanceState) {
548 super.onActivityCreated(savedInstanceState);
549
550 validateAction(mAction);
551
552 if (mState.isEmpty()) {
553 // The delta list may not have finished loading before orientation change happens.
554 // In this case, there will be a saved state but deltas will be missing. Reload from
555 // database.
556 if (Intent.ACTION_EDIT.equals(mAction)) {
557 // Either
558 // 1) orientation change but load never finished.
559 // 2) not an orientation change so data needs to be loaded for first time.
560 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
561 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
562 }
563 } else {
564 // Orientation change, we already have mState, it was loaded by onCreate
565 bindEditors();
566 }
567
568 // Handle initial actions only when existing state missing
569 if (savedInstanceState == null) {
Marcus Hagerott807e6202016-12-21 08:59:50 -0800570 if (mIntentExtras != null) {
571 final Account account = mIntentExtras == null ? null :
572 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
573 final String dataSet = mIntentExtras == null ? null :
574 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
575 mAccountWithDataSet = account != null
576 ? new AccountWithDataSet(account.name, account.type, dataSet)
577 : mIntentExtras.<AccountWithDataSet>getParcelable(
578 ContactEditorActivity.EXTRA_ACCOUNT_WITH_DATA_SET);
Walter Jang7b0970f2016-09-01 10:40:19 -0700579 }
580
581 if (Intent.ACTION_EDIT.equals(mAction)) {
582 mIsEdit = true;
583 } else if (Intent.ACTION_INSERT.equals(mAction)) {
584 mHasNewContact = true;
585 if (mAccountWithDataSet != null) {
586 createContact(mAccountWithDataSet);
Marcus Hagerott807e6202016-12-21 08:59:50 -0800587 } // else wait for accounts to be loaded
Walter Jang7b0970f2016-09-01 10:40:19 -0700588 }
589 }
Marcus Hagerott807e6202016-12-21 08:59:50 -0800590
591 if (mHasNewContact) {
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800592 AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter());
Marcus Hagerott807e6202016-12-21 08:59:50 -0800593 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700594 }
595
Marcus Hagerott98388512018-03-30 09:22:13 -0700596 @Override
597 public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
598 super.onViewStateRestored(savedInstanceState);
599 if (savedInstanceState == null) {
600 return;
601 }
602 maybeRestoreFocus(savedInstanceState);
603 }
604
Walter Jang7b0970f2016-09-01 10:40:19 -0700605 /**
606 * Checks if the requested action is valid.
607 *
608 * @param action The action to test.
609 * @throws IllegalArgumentException when the action is invalid.
610 */
611 private static void validateAction(String action) {
612 if (VALID_INTENT_ACTIONS.contains(action)) {
613 return;
614 }
615 throw new IllegalArgumentException(
616 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
617 }
618
619 @Override
Walter Jang3efae4a2015-02-18 11:12:00 -0800620 public void onSaveInstanceState(Bundle outState) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700621 outState.putString(KEY_ACTION, mAction);
622 outState.putParcelable(KEY_URI, mLookupUri);
623 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
624 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
625 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
626 if (mMaterialPalette != null) {
627 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
628 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700629 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
630
631 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
632 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
633 // NOTE: mGroupMetaData is not saved
634
Gary Mai36ceb422016-10-17 14:04:17 -0700635 outState.putParcelable(KEY_EDIT_STATE, mState);
Walter Jang7b0970f2016-09-01 10:40:19 -0700636 outState.putInt(KEY_STATUS, mStatus);
637 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
638 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
639 outState.putBoolean(KEY_IS_EDIT, mIsEdit);
640 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
Gary Maic135a5d2016-12-19 11:13:46 -0800641 outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
Walter Jang7b0970f2016-09-01 10:40:19 -0700642 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
643
Walter Jang7b0970f2016-09-01 10:40:19 -0700644 outState.putBoolean(KEY_ENABLED, mEnabled);
645
646 // Aggregation PopupWindow
647 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
648 mAggregationSuggestionsRawContactId);
649
650 // Join Activity
651 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
652
Gary Mai698cee72016-09-19 16:09:54 -0700653 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
654 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
Walter Jang7b0970f2016-09-01 10:40:19 -0700655
Walter Jang3efae4a2015-02-18 11:12:00 -0800656 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
Walter Jang28a27272015-09-19 16:06:08 -0700657 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
Marcus Hagerott98388512018-03-30 09:22:13 -0700658
659 // For b/77246197
660 View focusedView = getView() == null ? null : getView().findFocus();
661 if (focusedView != null) {
662 outState.putInt(KEY_FOCUSED_VIEW_ID, focusedView.getId());
663 outState.putBoolean(KEY_RESTORE_SOFT_INPUT, inputMethodManager.isActive(focusedView));
664 }
665
Walter Jang3efae4a2015-02-18 11:12:00 -0800666 super.onSaveInstanceState(outState);
667 }
668
669 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700670 public void onStop() {
671 super.onStop();
672 UiClosables.closeQuietly(mAggregationSuggestionPopup);
673 }
674
675 @Override
676 public void onDestroy() {
677 super.onDestroy();
678 if (mAggregationSuggestionEngine != null) {
679 mAggregationSuggestionEngine.quit();
680 }
681 }
682
683 @Override
684 public void onActivityResult(int requestCode, int resultCode, Intent data) {
685 switch (requestCode) {
686 case REQUEST_CODE_JOIN: {
687 // Ignore failed requests
688 if (resultCode != Activity.RESULT_OK) return;
689 if (data != null) {
690 final long contactId = ContentUris.parseId(data.getData());
691 if (hasPendingChanges()) {
692 // Ask the user if they want to save changes before doing the join
693 JoinContactConfirmationDialogFragment.show(this, contactId);
694 } else {
695 // Do the join immediately
696 joinAggregate(contactId);
697 }
698 }
699 break;
700 }
701 case REQUEST_CODE_ACCOUNTS_CHANGED: {
702 // Bail if the account selector was not successful.
Marcus Hagerott807e6202016-12-21 08:59:50 -0800703 if (resultCode != Activity.RESULT_OK || data == null ||
704 !data.hasExtra(Intents.Insert.EXTRA_ACCOUNT)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700705 if (mListener != null) {
706 mListener.onReverted();
707 }
708 return;
709 }
Marcus Hagerott807e6202016-12-21 08:59:50 -0800710 AccountWithDataSet account = data.getParcelableExtra(
711 Intents.Insert.EXTRA_ACCOUNT);
712 createContact(account);
Walter Jang7b0970f2016-09-01 10:40:19 -0700713 break;
714 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700715 }
716 }
717
Marcus Hagerott8c6b5bd2016-12-21 17:14:52 -0800718 @Override
719 public void onAccountsLoaded(List<AccountInfo> data) {
720 mWritableAccounts = data;
721 // The user may need to select a new account to save to
722 if (mAccountWithDataSet == null && mHasNewContact) {
723 selectAccountAndCreateContact();
724 }
725
726 final RawContactEditorView view = getContent();
727 if (view == null) {
728 return;
729 }
730 view.setAccounts(data);
731 if (mAccountWithDataSet == null && view.getCurrentRawContactDelta() == null) {
732 return;
733 }
734
735 final AccountWithDataSet account = mAccountWithDataSet != null
736 ? mAccountWithDataSet
737 : view.getCurrentRawContactDelta().getAccountWithDataSet();
738
739 // The current account was removed
740 if (!AccountInfo.contains(data, account) && !data.isEmpty()) {
741 if (isReadyToBindEditors()) {
742 onRebindEditorsForNewContact(getContent().getCurrentRawContactDelta(),
743 account, data.get(0).getAccount());
744 } else {
745 mAccountWithDataSet = data.get(0).getAccount();
746 }
747 }
748 }
749
Walter Jang7b0970f2016-09-01 10:40:19 -0700750 //
751 // Options menu
752 //
753
Walter Jang7b0970f2016-09-01 10:40:19 -0700754 @Override
755 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
756 inflater.inflate(R.menu.edit_contact, menu);
757 }
758
759 @Override
760 public void onPrepareOptionsMenu(Menu menu) {
761 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
762 // because the custom action bar contains the "save" button now (not the overflow menu).
763 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
764 final MenuItem saveMenu = menu.findItem(R.id.menu_save);
765 final MenuItem splitMenu = menu.findItem(R.id.menu_split);
766 final MenuItem joinMenu = menu.findItem(R.id.menu_join);
Walter Jang7b0970f2016-09-01 10:40:19 -0700767 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
768
Gary Mai5eda2572016-10-11 18:01:32 -0700769 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
770 // on a raw contact level.
771 joinMenu.setVisible(false);
772 splitMenu.setVisible(false);
773 deleteMenu.setVisible(false);
Walter Jang7b0970f2016-09-01 10:40:19 -0700774 // Save menu is invisible when there's only one read only contact in the editor.
Gary Maid7faa652016-10-03 11:53:39 -0700775 saveMenu.setVisible(!isEditingReadOnlyRawContact());
Walter Jang7b0970f2016-09-01 10:40:19 -0700776 if (saveMenu.isVisible()) {
777 // Since we're using a custom action layout we have to manually hook up the handler.
778 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
779 @Override
780 public void onClick(View v) {
781 onOptionsItemSelected(saveMenu);
782 }
783 });
784 }
785
John Shao476df402017-11-07 21:07:18 -0800786 final MenuItem helpMenu = menu.findItem(R.id.menu_help);
787 helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
788
Walter Jang7b0970f2016-09-01 10:40:19 -0700789 int size = menu.size();
790 for (int i = 0; i < size; i++) {
791 menu.getItem(i).setEnabled(mEnabled);
792 }
793 }
794
795 @Override
Walter Jangc90cc152015-06-19 14:15:08 -0700796 public boolean onOptionsItemSelected(MenuItem item) {
797 if (item.getItemId() == android.R.id.home) {
798 return revert();
799 }
Walter Jang7b0970f2016-09-01 10:40:19 -0700800
801 final Activity activity = getActivity();
802 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
803 // If we no longer are attached to a running activity want to
804 // drain this event.
805 return true;
806 }
807
Marcus Hagerottb697ed72016-12-20 15:02:56 -0800808 final int id = item.getItemId();
809 if (id == R.id.menu_save) {
810 return save(SaveMode.CLOSE);
811 } else if (id == R.id.menu_delete) {
812 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
813 return true;
814 } else if (id == R.id.menu_split) {
815 return doSplitContactAction();
816 } else if (id == R.id.menu_join) {
817 return doJoinContactAction();
818 } else if (id == R.id.menu_help) {
819 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
820 return true;
Walter Jang7b0970f2016-09-01 10:40:19 -0700821 }
822
823 return false;
Walter Jangc90cc152015-06-19 14:15:08 -0700824 }
825
826 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -0700827 public boolean revert() {
828 if (mState.isEmpty() || !hasPendingChanges()) {
829 onCancelEditConfirmed();
830 } else {
831 CancelEditDialogFragment.show(this);
832 }
833 return true;
834 }
835
836 @Override
837 public void onCancelEditConfirmed() {
838 // When this Fragment is closed we don't want it to auto-save
839 mStatus = Status.CLOSING;
840 if (mListener != null) {
841 mListener.onReverted();
842 }
843 }
844
845 @Override
846 public void onSplitContactConfirmed(boolean hasPendingChanges) {
847 if (mState.isEmpty()) {
848 // This may happen when this Fragment is recreated by the system during users
849 // confirming the split action (and thus this method is called just before onCreate()),
850 // for example.
851 Log.e(TAG, "mState became null during the user's confirming split action. " +
852 "Cannot perform the save action.");
853 return;
854 }
855
856 if (!hasPendingChanges && mHasNewContact) {
857 // If the user didn't add anything new, we don't want to split out the newly created
858 // raw contact into a name-only contact so remove them.
859 final Iterator<RawContactDelta> iterator = mState.iterator();
860 while (iterator.hasNext()) {
861 final RawContactDelta rawContactDelta = iterator.next();
862 if (rawContactDelta.getRawContactId() < 0) {
863 iterator.remove();
864 }
865 }
866 }
867 mState.markRawContactsForSplitting();
868 save(SaveMode.SPLIT);
869 }
870
Gary Maib9065dd2016-11-08 10:49:00 -0800871 @Override
872 public void onSplitContactCanceled() {}
873
Walter Jang7b0970f2016-09-01 10:40:19 -0700874 private boolean doSplitContactAction() {
875 if (!hasValidState()) return false;
876
877 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
878 return true;
879 }
880
881 private boolean doJoinContactAction() {
882 if (!hasValidState() || mLookupUri == null) {
883 return false;
884 }
885
886 // If we just started creating a new contact and haven't added any data, it's too
887 // early to do a join
888 if (mState.size() == 1 && mState.get(0).isContactInsert()
889 && !hasPendingChanges()) {
890 Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
891 Toast.LENGTH_LONG).show();
892 return true;
893 }
894
895 showJoinAggregateActivity(mLookupUri);
896 return true;
897 }
898
899 @Override
900 public void onJoinContactConfirmed(long joinContactId) {
901 doSaveAction(SaveMode.JOIN, joinContactId);
902 }
903
Walter Jang7b0970f2016-09-01 10:40:19 -0700904 @Override
905 public boolean save(int saveMode) {
906 if (!hasValidState() || mStatus != Status.EDITING) {
907 return false;
908 }
909
910 // If we are about to close the editor - there is no need to refresh the data
Gary Mai363af602016-09-28 10:01:23 -0700911 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
Walter Jang7b0970f2016-09-01 10:40:19 -0700912 || saveMode == SaveMode.SPLIT) {
913 getLoaderManager().destroyLoader(LOADER_CONTACT);
914 }
915
916 mStatus = Status.SAVING;
917
918 if (!hasPendingChanges()) {
919 if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
920 // We don't have anything to save and there isn't even an existing contact yet.
921 // Nothing to do, simply go back to editing mode
922 mStatus = Status.EDITING;
923 return true;
924 }
925 onSaveCompleted(/* hadChanges =*/ false, saveMode,
926 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
927 return true;
928 }
929
930 setEnabled(false);
931
932 return doSaveAction(saveMode, /* joinContactId */ null);
933 }
934
935 //
936 // State accessor methods
937 //
938
939 /**
940 * Check if our internal {@link #mState} is valid, usually checked before
941 * performing user actions.
942 */
943 private boolean hasValidState() {
944 return mState.size() > 0;
945 }
946
947 private boolean isEditingUserProfile() {
948 return mNewLocalProfile || mIsUserProfile;
949 }
950
951 /**
Gary Mai5a00de32016-10-19 18:20:41 -0700952 * Whether the contact being edited is composed of read-only raw contacts
Walter Jang7b0970f2016-09-01 10:40:19 -0700953 * aggregated with a newly created writable raw contact.
954 */
955 private boolean isEditingReadOnlyRawContactWithNewContact() {
Gary Mai5a00de32016-10-19 18:20:41 -0700956 return mHasNewContact && mState.size() > 1;
Walter Jang7b0970f2016-09-01 10:40:19 -0700957 }
958
959 /**
Gary Maid7faa652016-10-03 11:53:39 -0700960 * @return true if the single raw contact we're looking at is read-only.
961 */
962 private boolean isEditingReadOnlyRawContact() {
963 return hasValidState() && mRawContactIdToDisplayAlone > 0
964 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
965 .getAccountType(AccountTypeManager.getInstance(mContext))
966 .areContactsWritable();
967 }
968
969 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700970 * Return true if there are any edits to the current contact which need to
971 * be saved.
972 */
973 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
974 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
975 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
976 }
977
978 /**
Walter Jang7b0970f2016-09-01 10:40:19 -0700979 * Determines if changes were made in the editor that need to be saved, while taking into
980 * account that name changes are not real for read-only contacts.
981 * See go/editing-read-only-contacts
982 */
983 private boolean hasPendingChanges() {
Gary Mai698cee72016-09-19 16:09:54 -0700984 if (isEditingReadOnlyRawContactWithNewContact()) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700985 // We created a new raw contact delta with a default display name.
986 // We must test for pending changes while ignoring the default display name.
yingrenw91f15e02018-01-17 17:03:15 +0800987 final RawContactDelta beforeRawContactDelta = mState
988 .getByRawContactId(mReadOnlyDisplayNameId);
blong8407c932017-05-18 09:47:34 +0800989 final ValuesDelta beforeDelta = beforeRawContactDelta == null ? null :
990 beforeRawContactDelta.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
Gary Mai698cee72016-09-19 16:09:54 -0700991 final ValuesDelta pendingDelta = mState
992 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
993 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
Walter Jang7b0970f2016-09-01 10:40:19 -0700994 final Set<String> excludedMimeTypes = new HashSet<>();
995 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
996 return hasPendingRawContactChanges(excludedMimeTypes);
997 }
998 return true;
999 }
1000 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
1001 }
1002
1003 /**
Gary Mai698cee72016-09-19 16:09:54 -07001004 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
1005 * of a read only delta and now we want to check if the copied delta has changes.
1006 *
1007 * @param before original {@link ValuesDelta}
1008 * @param after copied {@link ValuesDelta}
1009 * @return true if the copied {@link ValuesDelta} has all the same values in the structured
1010 * name fields as the original.
1011 */
1012 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
Gary Mai5a00de32016-10-19 18:20:41 -07001013 if (before == after) return true;
Gary Mai698cee72016-09-19 16:09:54 -07001014 if (before == null || after == null) return false;
1015 final ContentValues original = before.getBefore();
1016 final ContentValues pending = after.getAfter();
1017 if (original != null && pending != null) {
Gary Maia4adae12016-10-23 13:47:17 -07001018 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
Gary Mai698cee72016-09-19 16:09:54 -07001019 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
1020 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
1021
1022 final String beforePrefix = original.getAsString(StructuredName.PREFIX);
1023 final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
1024 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
1025
1026 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
1027 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
1028 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
1029
1030 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
1031 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
1032 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
1033
1034 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
1035 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
1036 if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
1037
1038 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
1039 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
1040 return TextUtils.equals(beforeSuffix, afterSuffix);
1041 }
1042 return false;
1043 }
1044
Walter Jang7b0970f2016-09-01 10:40:19 -07001045 //
1046 // Account creation
1047 //
1048
1049 private void selectAccountAndCreateContact() {
Marcus Hagerott807e6202016-12-21 08:59:50 -08001050 Preconditions.checkNotNull(mWritableAccounts, "Accounts must be loaded first");
Walter Jang7b0970f2016-09-01 10:40:19 -07001051 // If this is a local profile, then skip the logic about showing the accounts changed
1052 // activity and create a phone-local contact.
1053 if (mNewLocalProfile) {
1054 createContact(null);
1055 return;
1056 }
1057
Marcus Hagerott807e6202016-12-21 08:59:50 -08001058 final List<AccountWithDataSet> accounts = AccountInfo.extractAccounts(mWritableAccounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001059 // If there is no default account or the accounts have changed such that we need to
1060 // prompt the user again, then launch the account prompt.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001061 if (mEditorUtils.shouldShowAccountChangedNotification(accounts)) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001062 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
1063 // Prevent a second instance from being started on rotates
Marcus Hagerott80ab7ea2017-01-03 13:19:56 -08001064 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Walter Jang7b0970f2016-09-01 10:40:19 -07001065 mStatus = Status.SUB_ACTIVITY;
1066 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
1067 } else {
Gary Mai3107b252016-11-02 18:26:07 -07001068 // Make sure the default account is automatically set if there is only one non-device
1069 // account.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001070 mEditorUtils.maybeUpdateDefaultAccount(accounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001071 // Otherwise, there should be a default account. Then either create a local contact
1072 // (if default account is null) or create a contact with the specified account.
Marcus Hagerott807e6202016-12-21 08:59:50 -08001073 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(accounts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001074 createContact(defaultAccount);
1075 }
1076 }
1077
1078 /**
Walter Jang7b0970f2016-09-01 10:40:19 -07001079 * Shows account creation screen associated with a given account.
1080 *
1081 * @param account may be null to signal a device-local contact should be created.
1082 */
1083 private void createContact(AccountWithDataSet account) {
1084 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1085 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1086
Gary Maiaebf3202016-09-22 18:11:15 -07001087 setStateForNewContact(account, accountType, isEditingUserProfile());
Walter Jang7b0970f2016-09-01 10:40:19 -07001088 }
1089
1090 //
1091 // Data binding
1092 //
1093
1094 private void setState(Contact contact) {
1095 // If we have already loaded data, we do not want to change it here to not confuse the user
1096 if (!mState.isEmpty()) {
Wenyi Wang57a0e982017-03-24 16:02:44 -07001097 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1098 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1099 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001100 return;
1101 }
Gary Mai7b751452016-11-07 17:04:04 -08001102 mContact = contact;
Gary Mai4ceabed2016-09-16 12:14:13 -07001103 mRawContacts = contact.getRawContacts();
Walter Jang7b0970f2016-09-01 10:40:19 -07001104
Walter Jang7b0970f2016-09-01 10:40:19 -07001105 // Check for writable raw contacts. If there are none, then we need to create one so user
1106 // can edit. For the user profile case, there is already an editable contact.
1107 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1108 mHasNewContact = true;
Gary Mai698cee72016-09-19 16:09:54 -07001109 mReadOnlyDisplayNameId = contact.getNameRawContactId();
1110 mCopyReadOnlyName = true;
Walter Jang7b0970f2016-09-01 10:40:19 -07001111 // This is potentially an asynchronous call and will add deltas to list.
1112 selectAccountAndCreateContact();
Walter Jang7b0970f2016-09-01 10:40:19 -07001113 } else {
1114 mHasNewContact = false;
1115 }
1116
Gary Mai698cee72016-09-19 16:09:54 -07001117 setStateForExistingContact(contact.isUserProfile(), mRawContacts);
Gary Maie4874662016-09-26 11:42:54 -07001118 if (mAutoAddToDefaultGroup
1119 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1120 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1121 }
Walter Jang7b0970f2016-09-01 10:40:19 -07001122 }
1123
1124 /**
1125 * Prepare {@link #mState} for a newly created phone-local contact.
1126 */
1127 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1128 boolean isUserProfile) {
1129 setStateForNewContact(account, accountType, /* oldState =*/ null,
1130 /* oldAccountType =*/ null, isUserProfile);
1131 }
1132
1133 /**
1134 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1135 * specified by oldState and oldAccountType.
1136 */
1137 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1138 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1139 mStatus = Status.EDITING;
Gary Mai9f691e82017-01-10 16:47:45 -08001140 mAccountWithDataSet = account;
Walter Jang7b0970f2016-09-01 10:40:19 -07001141 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1142 mIsUserProfile = isUserProfile;
1143 mNewContactDataReady = true;
1144 bindEditors();
1145 }
1146
1147 /**
1148 * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1149 * {@link #mState}.
1150 *
1151 * If oldState and oldAccountType are specified, the state specified by those parameters
1152 * is migrated to the result {@link RawContactDelta}.
1153 */
1154 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1155 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1156 final RawContact rawContact = new RawContact();
1157 if (account != null) {
1158 rawContact.setAccount(account);
1159 } else {
1160 rawContact.setAccountToLocal();
1161 }
1162
1163 final RawContactDelta result = new RawContactDelta(
1164 ValuesDelta.fromAfter(rawContact.getValues()));
1165 if (oldState == null) {
1166 // Parse any values from incoming intent
1167 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1168 } else {
1169 RawContactModifier.migrateStateForNewContact(
1170 mContext, oldState, result, oldAccountType, accountType);
1171 }
1172
1173 // Ensure we have some default fields (if the account type does not support a field,
1174 // ensureKind will not add it, so it is safe to add e.g. Event)
Gary Mai62ec0b12016-10-07 14:23:54 -07001175 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001176 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1177 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1178 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1179 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1180 RawContactModifier.ensureKindExists(result, accountType,
1181 StructuredPostal.CONTENT_ITEM_TYPE);
1182
1183 // Set the correct URI for saving the contact as a profile
1184 if (mNewLocalProfile) {
1185 result.setProfileQueryUri();
1186 }
1187
1188 return result;
1189 }
1190
1191 /**
1192 * Prepare {@link #mState} for an existing contact.
1193 */
Gary Mai698cee72016-09-19 16:09:54 -07001194 private void setStateForExistingContact(boolean isUserProfile,
Walter Jang7b0970f2016-09-01 10:40:19 -07001195 ImmutableList<RawContact> rawContacts) {
1196 setEnabled(true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001197
1198 mState.addAll(rawContacts.iterator());
1199 setIntentExtras(mIntentExtras);
1200 mIntentExtras = null;
1201
1202 // For user profile, change the contacts query URI
1203 mIsUserProfile = isUserProfile;
1204 boolean localProfileExists = false;
1205
1206 if (mIsUserProfile) {
1207 for (RawContactDelta rawContactDelta : mState) {
1208 // For profile contacts, we need a different query URI
1209 rawContactDelta.setProfileQueryUri();
1210 // Try to find a local profile contact
1211 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1212 localProfileExists = true;
1213 }
1214 }
1215 // Editor should always present a local profile for editing
1216 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
1217 // going to prune all but the one raw contact that we're trying to display by itself.
1218 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1219 mState.add(createLocalRawContactDelta());
1220 }
1221 }
1222 mExistingContactDataReady = true;
1223 bindEditors();
1224 }
1225
1226 /**
1227 * Set the enabled state of editors.
1228 */
1229 private void setEnabled(boolean enabled) {
1230 if (mEnabled != enabled) {
1231 mEnabled = enabled;
1232
1233 // Enable/disable editors
1234 if (mContent != null) {
1235 int count = mContent.getChildCount();
1236 for (int i = 0; i < count; i++) {
1237 mContent.getChildAt(i).setEnabled(enabled);
1238 }
1239 }
1240
Walter Jang7b0970f2016-09-01 10:40:19 -07001241 // Maybe invalidate the options menu
1242 final Activity activity = getActivity();
1243 if (activity != null) activity.invalidateOptionsMenu();
1244 }
1245 }
1246
1247 /**
1248 * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1249 * {@link #mState}.
1250 */
1251 private static RawContactDelta createLocalRawContactDelta() {
1252 final RawContact rawContact = new RawContact();
1253 rawContact.setAccountToLocal();
1254
1255 final RawContactDelta result = new RawContactDelta(
1256 ValuesDelta.fromAfter(rawContact.getValues()));
1257 result.setProfileQueryUri();
1258
1259 return result;
1260 }
1261
Gary Mai698cee72016-09-19 16:09:54 -07001262 private void copyReadOnlyName() {
1263 // We should only ever be doing this if we're creating a new writable contact to attach to
1264 // a read only contact.
1265 if (!isEditingReadOnlyRawContactWithNewContact()) {
1266 return;
1267 }
1268 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1269 final RawContactDelta writable = mState.get(writableIndex);
Gary Mai7b751452016-11-07 17:04:04 -08001270 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
Gary Mai698cee72016-09-19 16:09:54 -07001271 final ValuesDelta writeNameDelta = writable
1272 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1273 final ValuesDelta readNameDelta = readOnly
1274 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
Gary Mai698cee72016-09-19 16:09:54 -07001275 mCopyReadOnlyName = false;
Gary Mai860698b2016-12-19 11:44:57 -08001276 if (writeNameDelta == null || readNameDelta == null) {
1277 return;
1278 }
1279 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
Gary Mai698cee72016-09-19 16:09:54 -07001280 }
1281
Walter Jang7b0970f2016-09-01 10:40:19 -07001282 /**
1283 * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1284 * Contact.
1285 */
Walter Jangba59deb2015-01-26 11:23:48 -08001286 protected void bindEditors() {
Walter Jangcab3dce2015-02-09 17:48:03 -08001287 if (!isReadyToBindEditors()) {
1288 return;
1289 }
1290
Walter Jangd35e5ef2015-02-24 09:18:16 -08001291 // Add input fields for the loaded Contact
Gary Mai363af602016-09-28 10:01:23 -07001292 final RawContactEditorView editorView = getContent();
Walter Jangb6ca2722015-02-20 11:10:25 -08001293 editorView.setListener(this);
Gary Mai698cee72016-09-19 16:09:54 -07001294 if (mCopyReadOnlyName) {
1295 copyReadOnlyName();
1296 }
Gary Mai678108e2016-10-26 14:34:33 -07001297 editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
Walter Jang9a552372016-08-24 11:51:05 -07001298 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
Gary Mai5a00de32016-10-19 18:20:41 -07001299 mRawContactIdToDisplayAlone);
Gary Mai079598f2016-11-03 15:02:45 -07001300 if (isEditingReadOnlyRawContact()) {
Gary Mai15646ce2016-11-17 10:54:01 -08001301 final Toolbar toolbar = getEditorActivity().getToolbar();
1302 if (toolbar != null) {
1303 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
Gary Maid8f3da62016-11-18 11:47:20 -08001304 // Set activity title for Talkback
1305 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
John Shaobd9ef3c2016-12-15 12:42:03 -08001306 toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
Gary Mai15646ce2016-11-17 10:54:01 -08001307 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
John Shaobd9ef3c2016-12-15 12:42:03 -08001308 toolbar.getNavigationIcon().setAutoMirrored(true);
Gary Mai079598f2016-11-03 15:02:45 -07001309 }
1310 }
Walter Jangcab3dce2015-02-09 17:48:03 -08001311
Walter Jangd35e5ef2015-02-24 09:18:16 -08001312 // Set up the photo widget
Walter Jang31a74ad2015-10-02 19:17:39 -07001313 editorView.setPhotoListener(this);
Walter Jang3efae4a2015-02-18 11:12:00 -08001314 mPhotoRawContactId = editorView.getPhotoRawContactId();
Walter Jang31a74ad2015-10-02 19:17:39 -07001315 // If there is an updated full resolution photo apply it now, this will be the case if
1316 // the user selects or takes a new photo, then rotates the device.
1317 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1318 if (uri != null) {
1319 editorView.setFullSizePhoto(uri);
Walter Jang41b3ea12015-03-09 17:30:06 -07001320 }
yoichi kakimoto5ed462a2017-09-04 19:20:13 +09001321 final StructuredNameEditorView nameEditor = editorView.getNameEditorView();
1322 final TextFieldsEditorView phoneticNameEditor = editorView.getPhoneticEditorView();
1323 final boolean useJapaneseOrder =
1324 Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
1325 if (useJapaneseOrder && nameEditor != null && phoneticNameEditor != null) {
1326 nameEditor.setPhoneticView(phoneticNameEditor);
1327 }
Walter Jang3efae4a2015-02-18 11:12:00 -08001328
Walter Jangd35e5ef2015-02-24 09:18:16 -08001329 // The editor is ready now so make it visible
Gary Mai678108e2016-10-26 14:34:33 -07001330 editorView.setEnabled(mEnabled);
Walter Jangd35e5ef2015-02-24 09:18:16 -08001331 editorView.setVisibility(View.VISIBLE);
1332
1333 // Refresh the ActionBar as the visibility of the join command
1334 // Activity can be null if we have been detached from the Activity.
Walter Jangcab3dce2015-02-09 17:48:03 -08001335 invalidateOptionsMenu();
1336 }
1337
Walter Jang7b0970f2016-09-01 10:40:19 -07001338 /**
1339 * Invalidates the options menu if we are still associated with an Activity.
1340 */
1341 private void invalidateOptionsMenu() {
1342 final Activity activity = getActivity();
1343 if (activity != null) {
1344 activity.invalidateOptionsMenu();
1345 }
1346 }
1347
Walter Jangcab3dce2015-02-09 17:48:03 -08001348 private boolean isReadyToBindEditors() {
1349 if (mState.isEmpty()) {
1350 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1351 Log.v(TAG, "No data to bind editors");
1352 }
1353 return false;
1354 }
1355 if (mIsEdit && !mExistingContactDataReady) {
1356 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1357 Log.v(TAG, "Existing contact data is not ready to bind editors.");
1358 }
1359 return false;
1360 }
1361 if (mHasNewContact && !mNewContactDataReady) {
1362 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1363 Log.v(TAG, "New contact data is not ready to bind editors.");
1364 }
1365 return false;
1366 }
Gary Mai1e899dc2017-02-07 15:08:53 -08001367 // Don't attempt to bind anything if we have no permissions.
1368 return RequestPermissionsActivity.hasRequiredPermissions(mContext);
Walter Jangba59deb2015-01-26 11:23:48 -08001369 }
1370
Walter Jang7b0970f2016-09-01 10:40:19 -07001371 /**
1372 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1373 * Some of old data are reused with new restriction enforced by the new account.
1374 *
1375 * @param oldState Old data being edited.
1376 * @param oldAccount Old account associated with oldState.
1377 * @param newAccount New account to be used.
1378 */
1379 private void rebindEditorsForNewContact(
1380 RawContactDelta oldState, AccountWithDataSet oldAccount,
1381 AccountWithDataSet newAccount) {
1382 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1383 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1384 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1385
Gary Maiaebf3202016-09-22 18:11:15 -07001386 mExistingContactDataReady = false;
1387 mNewContactDataReady = false;
1388 mState = new RawContactDeltaList();
1389 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1390 isEditingUserProfile());
1391 if (mIsEdit) {
Gary Mai698cee72016-09-19 16:09:54 -07001392 setStateForExistingContact(isEditingUserProfile(), mRawContacts);
Walter Jang7b0970f2016-09-01 10:40:19 -07001393 }
1394 }
1395
1396 //
1397 // ContactEditor
1398 //
1399
Walter Jang3f990ba2015-01-27 17:38:30 +00001400 @Override
Walter Jang7b0970f2016-09-01 10:40:19 -07001401 public void setListener(Listener listener) {
1402 mListener = listener;
1403 }
1404
1405 @Override
1406 public void load(String action, Uri lookupUri, Bundle intentExtras) {
1407 mAction = action;
1408 mLookupUri = lookupUri;
1409 mIntentExtras = intentExtras;
1410
1411 if (mIntentExtras != null) {
1412 mAutoAddToDefaultGroup =
1413 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1414 mNewLocalProfile =
1415 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1416 mDisableDeleteMenuOption =
1417 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1418 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1419 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1420 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1421 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1422 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1423 }
Gary Maia6c80b32016-09-30 16:34:55 -07001424 mRawContactIdToDisplayAlone = mIntentExtras
1425 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
Walter Jang7b0970f2016-09-01 10:40:19 -07001426 }
1427 }
1428
1429 @Override
1430 public void setIntentExtras(Bundle extras) {
Gary Mai5336e6e2016-10-23 14:17:03 -07001431 getContent().setIntentExtras(extras);
Walter Jang7b0970f2016-09-01 10:40:19 -07001432 }
1433
1434 @Override
1435 public void onJoinCompleted(Uri uri) {
1436 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1437 }
1438
James Laskeye5a140a2016-10-18 15:43:42 -07001439
1440 private String getNameToDisplay(Uri contactUri) {
Gary Maic000d2e2016-11-18 13:51:17 -08001441 // The contact has been deleted or the uri is otherwise no longer right.
1442 if (contactUri == null) {
1443 return null;
1444 }
James Laskeye5a140a2016-10-18 15:43:42 -07001445 final ContentResolver resolver = mContext.getContentResolver();
1446 final Cursor cursor = resolver.query(contactUri, new String[]{
1447 ContactsContract.Contacts.DISPLAY_NAME,
1448 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
James Laskeye5a140a2016-10-18 15:43:42 -07001449
Gary Maia4adae12016-10-23 13:47:17 -07001450 if (cursor != null) {
1451 try {
1452 if (cursor.moveToFirst()) {
1453 final String displayName = cursor.getString(0);
1454 final String displayNameAlt = cursor.getString(1);
1455 cursor.close();
1456 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1457 new ContactsPreferences(mContext));
1458 }
1459 } finally {
1460 cursor.close();
1461 }
1462 }
James Laskeye5a140a2016-10-18 15:43:42 -07001463 return null;
1464 }
1465
1466
Walter Jang7b0970f2016-09-01 10:40:19 -07001467 @Override
1468 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1469 Uri contactLookupUri, Long joinContactId) {
1470 if (hadChanges) {
1471 if (saveSucceeded) {
1472 switch (saveMode) {
1473 case SaveMode.JOIN:
1474 break;
1475 case SaveMode.SPLIT:
1476 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1477 .show();
1478 break;
1479 default:
James Laskeye5a140a2016-10-18 15:43:42 -07001480 final String displayName = getNameToDisplay(contactLookupUri);
James Laskeyb1671052016-09-16 13:57:21 -07001481 final String toastMessage;
1482 if (!TextUtils.isEmpty(displayName)) {
1483 toastMessage = getResources().getString(
1484 R.string.contactSavedNamedToast, displayName);
1485 } else {
1486 toastMessage = getResources().getString(R.string.contactSavedToast);
1487 }
1488 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
Walter Jang7b0970f2016-09-01 10:40:19 -07001489 }
1490
1491 } else {
1492 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1493 }
1494 }
1495 switch (saveMode) {
1496 case SaveMode.CLOSE: {
Walter Jang8d26c0a2017-02-08 10:56:04 -08001497 final Intent resultIntent;
Walter Jang7b0970f2016-09-01 10:40:19 -07001498 if (saveSucceeded && contactLookupUri != null) {
1499 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1500 mContext, contactLookupUri, mLookupUri);
Walter Jang8d26c0a2017-02-08 10:56:04 -08001501 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1502 mContext, lookupUri, ScreenType.EDITOR);
1503 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
Walter Jang7b0970f2016-09-01 10:40:19 -07001504 } else {
1505 resultIntent = null;
1506 }
1507 // It is already saved, so prevent it from being saved again
1508 mStatus = Status.CLOSING;
1509 if (mListener != null) mListener.onSaveFinished(resultIntent);
1510 break;
1511 }
Gary Mai363af602016-09-28 10:01:23 -07001512 case SaveMode.EDITOR: {
Walter Jang7b0970f2016-09-01 10:40:19 -07001513 // It is already saved, so prevent it from being saved again
1514 mStatus = Status.CLOSING;
1515 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1516 break;
1517 }
1518 case SaveMode.JOIN:
1519 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1520 joinAggregate(joinContactId);
1521 }
1522 break;
1523 case SaveMode.RELOAD:
1524 if (saveSucceeded && contactLookupUri != null) {
1525 // If this was in INSERT, we are changing into an EDIT now.
1526 // If it already was an EDIT, we are changing to the new Uri now
1527 mState = new RawContactDeltaList();
1528 load(Intent.ACTION_EDIT, contactLookupUri, null);
1529 mStatus = Status.LOADING;
1530 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1531 }
1532 break;
1533
1534 case SaveMode.SPLIT:
1535 mStatus = Status.CLOSING;
1536 if (mListener != null) {
1537 mListener.onContactSplit(contactLookupUri);
Wenyi Wang57a0e982017-03-24 16:02:44 -07001538 } else if (Log.isLoggable(TAG, Log.DEBUG)) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001539 Log.d(TAG, "No listener registered, can not call onSplitFinished");
1540 }
1541 break;
1542 }
1543 }
1544
1545 /**
1546 * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1547 *
1548 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1549 */
1550 private void showJoinAggregateActivity(Uri contactLookupUri) {
1551 if (contactLookupUri == null || !isAdded()) {
1552 return;
1553 }
1554
1555 mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1556 final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1557 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1558 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1559 startActivityForResult(intent, REQUEST_CODE_JOIN);
1560 }
1561
1562 //
1563 // Aggregation PopupWindow
1564 //
1565
1566 /**
1567 * Triggers an asynchronous search for aggregation suggestions.
1568 */
1569 protected void acquireAggregationSuggestions(Context context,
1570 long rawContactId, ValuesDelta valuesDelta) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001571 mAggregationSuggestionsRawContactId = rawContactId;
1572
1573 if (mAggregationSuggestionEngine == null) {
1574 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1575 mAggregationSuggestionEngine.setListener(this);
1576 mAggregationSuggestionEngine.start();
1577 }
1578
1579 mAggregationSuggestionEngine.setContactId(getContactId());
Gary Mai220d10c2016-09-23 13:56:39 -07001580 mAggregationSuggestionEngine.setAccountFilter(
1581 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
Walter Jang7b0970f2016-09-01 10:40:19 -07001582
1583 mAggregationSuggestionEngine.onNameChange(valuesDelta);
1584 }
1585
1586 /**
1587 * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1588 */
1589 private long getContactId() {
1590 for (RawContactDelta rawContact : mState) {
1591 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1592 if (contactId != null) {
1593 return contactId;
1594 }
1595 }
1596 return 0;
1597 }
1598
1599 @Override
1600 public void onAggregationSuggestionChange() {
1601 final Activity activity = getActivity();
1602 if ((activity != null && activity.isFinishing())
1603 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
1604 return;
1605 }
1606
1607 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1608
1609 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1610 return;
1611 }
1612
Gary Maida20b472016-09-20 14:46:40 -07001613 final View anchorView = getAggregationAnchorView();
Walter Jang7b0970f2016-09-01 10:40:19 -07001614 if (anchorView == null) {
1615 return; // Raw contact deleted?
1616 }
1617 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1618 mAggregationSuggestionPopup.setAnchorView(anchorView);
1619 mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1620 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1621 mAggregationSuggestionPopup.setAdapter(
1622 new AggregationSuggestionAdapter(
1623 getActivity(),
Walter Jang7b0970f2016-09-01 10:40:19 -07001624 /* listener =*/ this,
1625 mAggregationSuggestionEngine.getSuggestions()));
1626 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1627 @Override
1628 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1629 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1630 suggestionView.handleItemClickEvent();
1631 UiClosables.closeQuietly(mAggregationSuggestionPopup);
1632 mAggregationSuggestionPopup = null;
1633 }
1634 });
1635 mAggregationSuggestionPopup.show();
1636 }
1637
1638 /**
Gary Maida20b472016-09-20 14:46:40 -07001639 * Returns the editor view that should be used as the anchor for aggregation suggestions.
Walter Jang7b0970f2016-09-01 10:40:19 -07001640 */
Gary Maida20b472016-09-20 14:46:40 -07001641 protected View getAggregationAnchorView() {
Walter Jangd35e5ef2015-02-24 09:18:16 -08001642 return getContent().getAggregationAnchorView();
1643 }
1644
Walter Jang7b0970f2016-09-01 10:40:19 -07001645 /**
1646 * Joins the suggested contact (specified by the id's of constituent raw
1647 * contacts), save all changes, and stay in the editor.
1648 */
1649 public void doJoinSuggestedContact(long[] rawContactIds) {
1650 if (!hasValidState() || mStatus != Status.EDITING) {
1651 return;
1652 }
1653
1654 mState.setJoinWithRawContacts(rawContactIds);
1655 save(SaveMode.RELOAD);
1656 }
1657
1658 @Override
Gary Mai678108e2016-10-26 14:34:33 -07001659 public void onEditAction(Uri contactLookupUri, long rawContactId) {
1660 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
Walter Jang7b0970f2016-09-01 10:40:19 -07001661 }
1662
1663 /**
Gary Mai678108e2016-10-26 14:34:33 -07001664 * Abandons the currently edited contact and switches to editing the selected raw contact,
1665 * transferring all the data there
Walter Jang7b0970f2016-09-01 10:40:19 -07001666 */
Gary Mai678108e2016-10-26 14:34:33 -07001667 public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
Walter Jang7b0970f2016-09-01 10:40:19 -07001668 if (mListener != null) {
1669 // make sure we don't save this contact when closing down
1670 mStatus = Status.CLOSING;
Gary Mai678108e2016-10-26 14:34:33 -07001671 mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1672 getContent().getCurrentRawContactDelta().getContentValues());
Walter Jang7b0970f2016-09-01 10:40:19 -07001673 }
1674 }
1675
1676 /**
1677 * Sets group metadata on all bound editors.
1678 */
Walter Jang92f8ccc2015-02-06 10:23:37 -08001679 protected void setGroupMetaData() {
Walter Jangf10ca152015-09-22 15:23:55 -07001680 if (mGroupMetaData != null) {
1681 getContent().setGroupMetaData(mGroupMetaData);
1682 }
Walter Jang3f990ba2015-01-27 17:38:30 +00001683 }
1684
Walter Jang7b0970f2016-09-01 10:40:19 -07001685 /**
1686 * Persist the accumulated editor deltas.
1687 *
1688 * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1689 * may be null.
1690 */
Walter Jange3373dc2015-10-27 15:35:12 -07001691 protected boolean doSaveAction(int saveMode, Long joinContactId) {
Walter Jang49ed2032015-02-11 20:09:05 -08001692 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1693 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1694 ((Activity) mContext).getClass(),
Gary Mai363af602016-09-28 10:01:23 -07001695 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
Walter Jange3373dc2015-10-27 15:35:12 -07001696 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
Wenyi Wangdd7d4562015-12-08 13:33:43 -08001697 return startSaveService(mContext, intent, saveMode);
Walter Jang49ed2032015-02-11 20:09:05 -08001698 }
1699
Walter Jang7b0970f2016-09-01 10:40:19 -07001700 private boolean startSaveService(Context context, Intent intent, int saveMode) {
1701 final boolean result = ContactSaveService.startService(
1702 context, intent, saveMode);
1703 if (!result) {
1704 onCancelEditConfirmed();
1705 }
1706 return result;
1707 }
1708
1709 //
1710 // Join Activity
1711 //
1712
1713 /**
1714 * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1715 */
Walter Jang49ed2032015-02-11 20:09:05 -08001716 protected void joinAggregate(final long contactId) {
1717 final Intent intent = ContactSaveService.createJoinContactsIntent(
Gary Mai363af602016-09-28 10:01:23 -07001718 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1719 ContactEditorActivity.ACTION_JOIN_COMPLETED);
Walter Jang49ed2032015-02-11 20:09:05 -08001720 mContext.startService(intent);
Walter Jang3f990ba2015-01-27 17:38:30 +00001721 }
Walter Jangb6ca2722015-02-20 11:10:25 -08001722
Walter Jang31a74ad2015-10-02 19:17:39 -07001723 public void removePhoto() {
1724 getContent().removePhoto();
1725 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
Walter Jang0e72ce92015-02-23 12:27:21 -08001726 }
1727
Walter Jang31a74ad2015-10-02 19:17:39 -07001728 public void updatePhoto(Uri uri) throws FileNotFoundException {
1729 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1730 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Wenyi Wang9bc9ba82015-11-17 19:37:33 -08001731 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Walter Jang31a74ad2015-10-02 19:17:39 -07001732 Toast.LENGTH_SHORT).show();
1733 return;
Walter Jang0e72ce92015-02-23 12:27:21 -08001734 }
Walter Jang31a74ad2015-10-02 19:17:39 -07001735 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1736 getContent().updatePhoto(uri);
Walter Jang0e72ce92015-02-23 12:27:21 -08001737 }
1738
Gary Maida20b472016-09-20 14:46:40 -07001739 public void setPrimaryPhoto() {
1740 getContent().setPrimaryPhoto();
Walter Jang0e72ce92015-02-23 12:27:21 -08001741 }
1742
1743 @Override
Walter Jang151f3e62015-02-26 15:29:40 -08001744 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1745 final Activity activity = getActivity();
1746 if (activity == null || activity.isFinishing()) {
1747 return;
1748 }
Walter Jang45b86d52015-10-15 15:23:16 -07001749 acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
Walter Jang151f3e62015-02-26 15:29:40 -08001750 }
1751
Walter Jang5a7a23b2015-03-06 10:54:26 -08001752 @Override
Walter Jang708ea9e2015-09-10 15:42:05 -07001753 public void onRebindEditorsForNewContact(RawContactDelta oldState,
1754 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1755 mNewContactAccountChanged = true;
Walter Jang708ea9e2015-09-10 15:42:05 -07001756 rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1757 }
1758
Walter Jang79658e12015-09-24 10:36:26 -07001759 @Override
1760 public void onBindEditorsFailed() {
1761 final Activity activity = getActivity();
1762 if (activity != null && !activity.isFinishing()) {
Gary Mai363af602016-09-28 10:01:23 -07001763 Toast.makeText(activity, R.string.editor_failed_to_load,
Walter Jang79658e12015-09-24 10:36:26 -07001764 Toast.LENGTH_SHORT).show();
1765 activity.setResult(Activity.RESULT_CANCELED);
1766 activity.finish();
1767 }
1768 }
1769
Walter Jangd6753152015-10-02 09:23:13 -07001770 @Override
1771 public void onEditorsBound() {
Wenyi Wang3cb77bb2016-07-27 17:39:03 -07001772 final Activity activity = getActivity();
1773 if (activity == null || activity.isFinishing()) {
1774 return;
1775 }
Walter Jangd6753152015-10-02 09:23:13 -07001776 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1777 }
1778
Walter Jang31a74ad2015-10-02 19:17:39 -07001779 @Override
1780 public void onPhotoEditorViewClicked() {
Walter Jang3f18d612015-10-07 16:01:05 -07001781 // For contacts composed of a single writable raw contact, or raw contacts have no more
1782 // than 1 photo, clicking the photo view simply opens the source photo dialog
Walter Jang31a74ad2015-10-02 19:17:39 -07001783 getEditorActivity().changePhoto(getPhotoMode());
1784 }
1785
yingrenw91f15e02018-01-17 17:03:15 +08001786 @Override
1787 public void onClearPhotoCache() {
1788 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
1789 }
1790
Walter Jang31a74ad2015-10-02 19:17:39 -07001791 private int getPhotoMode() {
Gary Maida20b472016-09-20 14:46:40 -07001792 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1793 : PhotoActionPopup.Modes.NO_PHOTO;
Walter Jang31a74ad2015-10-02 19:17:39 -07001794 }
1795
Gary Mai363af602016-09-28 10:01:23 -07001796 private ContactEditorActivity getEditorActivity() {
1797 return (ContactEditorActivity) getActivity();
Walter Jang31a74ad2015-10-02 19:17:39 -07001798 }
1799
Gary Mai363af602016-09-28 10:01:23 -07001800 private RawContactEditorView getContent() {
1801 return (RawContactEditorView) mContent;
Walter Jang3efae4a2015-02-18 11:12:00 -08001802 }
Marcus Hagerott98388512018-03-30 09:22:13 -07001803
1804 // TODO(b/77246197): figure out a better way to address focus being lost on rotation.
1805 private void maybeRestoreFocus(Bundle savedInstanceState) {
1806 int focusedViewId = savedInstanceState.getInt(KEY_FOCUSED_VIEW_ID, View.NO_ID);
1807 if (focusedViewId == View.NO_ID) {
1808 return;
1809 }
1810 boolean shouldRestoreSoftInput = savedInstanceState.getBoolean(KEY_RESTORE_SOFT_INPUT);
1811 new Handler()
1812 .postDelayed(
1813 () -> {
1814 if (!isResumed()) {
1815 return;
1816 }
1817 View root = getView();
1818 if (root == null) {
1819 return;
1820 }
1821 View focusedView = root.findFocus();
1822 if (focusedView != null) {
1823 return;
1824 }
1825 focusedView = getView().findViewById(focusedViewId);
1826 if (focusedView == null) {
1827 return;
1828 }
1829 boolean didFocus = focusedView.requestFocus();
1830 if (!didFocus) {
1831 Log.i(TAG, "requestFocus failed");
1832 return;
1833 }
1834 if (shouldRestoreSoftInput) {
1835 boolean didShow = inputMethodManager
1836 .showSoftInput(focusedView, InputMethodManager.SHOW_IMPLICIT);
1837 if (Log.isLoggable(TAG, Log.DEBUG)) {
1838 Log.d(TAG, "showSoftInput -> " + didShow);
1839 }
1840 }
1841 },
1842 RESTORE_FOCUS_DELAY_MILLIS);
1843 }
1844
Walter Jang3f990ba2015-01-27 17:38:30 +00001845}