blob: ce4b9bcc2e8ec2ccbd5efd7462804d85de8eaae2 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.contacts.editor;
import android.accounts.Account;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.RawContacts;
import androidx.appcompat.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListPopupWindow;
import android.widget.Toast;
import com.android.contacts.ContactSaveService;
import com.android.contacts.GroupMetaDataLoader;
import com.android.contacts.R;
import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
import com.android.contacts.activities.ContactEditorActivity;
import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
import com.android.contacts.activities.ContactSelectionActivity;
import com.android.contacts.activities.RequestPermissionsActivity;
import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
import com.android.contacts.group.GroupUtil;
import com.android.contacts.list.UiIntentActions;
import com.android.contacts.logging.ScreenEvent.ScreenType;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.Contact;
import com.android.contacts.model.ContactLoader;
import com.android.contacts.model.RawContact;
import com.android.contacts.model.RawContactDelta;
import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.model.RawContactModifier;
import com.android.contacts.model.ValuesDelta;
import com.android.contacts.model.account.AccountInfo;
import com.android.contacts.model.account.AccountType;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.model.account.AccountsLoader;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.quickcontact.InvisibleContactUtil;
import com.android.contacts.quickcontact.QuickContactActivity;
import com.android.contacts.util.ContactDisplayUtils;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.ImplicitIntentsUtil;
import com.android.contacts.util.MaterialColorMapUtils;
import com.android.contacts.util.UiClosables;
import com.android.contactsbind.HelpUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Contact editor with only the most important fields displayed initially.
*/
public class ContactEditorFragment extends Fragment implements
ContactEditor, SplitContactConfirmationDialogFragment.Listener,
JoinContactConfirmationDialogFragment.Listener,
AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
CancelEditDialogFragment.Listener,
RawContactEditorView.Listener, PhotoEditorView.Listener,
AccountsLoader.AccountsListener {
static final String TAG = "ContactEditor";
private static final int LOADER_CONTACT = 1;
private static final int LOADER_GROUPS = 2;
private static final int LOADER_ACCOUNTS = 3;
// How long to delay before attempting to restore focus and keyboard
// visibility after view state has been restored (e.g. after rotation)
// See b/77246197
private static final long RESTORE_FOCUS_DELAY_MILLIS = 100L;
private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
private static final String KEY_UPDATED_PHOTOS = "updated_photos";
private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
add(Intent.ACTION_EDIT);
add(Intent.ACTION_INSERT);
add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
}};
private static final String KEY_ACTION = "action";
private static final String KEY_URI = "uri";
private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
private static final String KEY_MATERIAL_PALETTE = "materialPalette";
private static final String KEY_ACCOUNT = "saveToAccount";
private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
private static final String KEY_RAW_CONTACTS = "rawContacts";
private static final String KEY_EDIT_STATE = "state";
private static final String KEY_STATUS = "status";
private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
private static final String KEY_IS_EDIT = "isEdit";
private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
private static final String KEY_IS_USER_PROFILE = "isUserProfile";
private static final String KEY_ENABLED = "enabled";
// Aggregation PopupWindow
private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
"aggregationSuggestionsRawContactId";
// Join Activity
private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
private static final String KEY_FOCUSED_VIEW_ID = "focusedViewId";
private static final String KEY_RESTORE_SOFT_INPUT = "restoreSoftInput";
protected static final int REQUEST_CODE_JOIN = 0;
protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
/**
* An intent extra that forces the editor to add the edited contact
* to the default group (e.g. "My Contacts").
*/
public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
"disableDeleteMenuOption";
/**
* Intent key to pass the photo palette primary color calculated by
* {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
*/
public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
"material_palette_primary_color";
/**
* Intent key to pass the photo palette secondary color calculated by
* {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
*/
public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
"material_palette_secondary_color";
/**
* Intent key to pass the ID of the photo to display on the editor.
*/
// TODO: This can be cleaned up if we decide to not pass the photo id through
// QuickContactActivity.
public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
/**
* Intent key to pass the ID of the raw contact id that should be displayed in the full editor
* by itself.
*/
public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
"raw_contact_id_to_display_alone";
/**
* Intent extra to specify a {@link ContactEditor.SaveMode}.
*/
public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
/**
* Intent extra key for the contact ID to join the current contact to after saving.
*/
public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
/**
* Callbacks for Activities that host contact editors Fragments.
*/
public interface Listener {
/**
* Contact was not found, so somehow close this fragment. This is raised after a contact
* is removed via Menu/Delete
*/
void onContactNotFound();
/**
* Contact was split, so we can close now.
*
* @param newLookupUri The lookup uri of the new contact that should be shown to the user.
* The editor tries best to chose the most natural contact here.
*/
void onContactSplit(Uri newLookupUri);
/**
* User has tapped Revert, close the fragment now.
*/
void onReverted();
/**
* Contact was saved and the Fragment can now be closed safely.
*/
void onSaveFinished(Intent resultIntent);
/**
* User switched to editing a different raw contact (a suggestion from the
* aggregation engine).
*/
void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
ArrayList<ContentValues> contentValues);
/**
* User has requested that contact be deleted.
*/
void onDeleteRequested(Uri contactUri);
}
/**
* Adapter for aggregation suggestions displayed in a PopupWindow when
* editor fields change.
*/
private static final class AggregationSuggestionAdapter extends BaseAdapter {
private final LayoutInflater mLayoutInflater;
private final AggregationSuggestionView.Listener mListener;
private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
public AggregationSuggestionAdapter(Activity activity,
AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
mLayoutInflater = activity.getLayoutInflater();
mListener = listener;
mSuggestions = suggestions;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Suggestion suggestion = (Suggestion) getItem(position);
final AggregationSuggestionView suggestionView =
(AggregationSuggestionView) mLayoutInflater.inflate(
R.layout.aggregation_suggestions_item, null);
suggestionView.setListener(mListener);
suggestionView.bindSuggestion(suggestion);
return suggestionView;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public Object getItem(int position) {
return mSuggestions.get(position);
}
@Override
public int getCount() {
return mSuggestions.size();
}
}
protected Context mContext;
protected Listener mListener;
//
// Views
//
protected LinearLayout mContent;
protected ListPopupWindow mAggregationSuggestionPopup;
//
// Parameters passed in on {@link #load}
//
protected String mAction;
protected Uri mLookupUri;
protected Bundle mIntentExtras;
protected boolean mAutoAddToDefaultGroup;
protected boolean mDisableDeleteMenuOption;
protected boolean mNewLocalProfile;
protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
//
// Helpers
//
protected ContactEditorUtils mEditorUtils;
protected RawContactDeltaComparator mComparator;
protected ViewIdGenerator mViewIdGenerator;
private AggregationSuggestionEngine mAggregationSuggestionEngine;
//
// Loaded data
//
// Used to store existing contact data so it can be re-applied during a rebind call,
// i.e. account switch.
protected Contact mContact;
protected ImmutableList<RawContact> mRawContacts;
protected Cursor mGroupMetaData;
//
// Editor state
//
protected RawContactDeltaList mState;
protected int mStatus;
protected long mRawContactIdToDisplayAlone = -1;
// Whether to show the new contact blank form and if it's corresponding delta is ready.
protected boolean mHasNewContact;
protected AccountWithDataSet mAccountWithDataSet;
protected List<AccountInfo> mWritableAccounts = Collections.emptyList();
protected boolean mNewContactDataReady;
protected boolean mNewContactAccountChanged;
// Whether it's an edit of existing contact and if it's corresponding delta is ready.
protected boolean mIsEdit;
protected boolean mExistingContactDataReady;
// Whether we are editing the "me" profile
protected boolean mIsUserProfile;
// Whether editor views and options menu items should be enabled
private boolean mEnabled = true;
// Aggregation PopupWindow
private long mAggregationSuggestionsRawContactId;
// Join Activity
protected long mContactIdForJoin;
// Used to pre-populate the editor with a display name when a user edits a read-only contact.
protected long mReadOnlyDisplayNameId;
protected boolean mCopyReadOnlyName;
/**
* The contact data loader listener.
*/
protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
new LoaderManager.LoaderCallbacks<Contact>() {
protected long mLoaderStartTime;
@Override
public Loader<Contact> onCreateLoader(int id, Bundle args) {
mLoaderStartTime = SystemClock.elapsedRealtime();
return new ContactLoader(mContext, mLookupUri,
/* postViewNotification */ true,
/* loadGroupMetaData */ true);
}
@Override
public void onLoadFinished(Loader<Contact> loader, Contact contact) {
final long loaderCurrentTime = SystemClock.elapsedRealtime();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG,
"Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
}
if (!contact.isLoaded()) {
// Item has been deleted. Close activity without saving again.
Log.i(TAG, "No contact found. Closing activity");
mStatus = Status.CLOSING;
if (mListener != null) mListener.onContactNotFound();
return;
}
mStatus = Status.EDITING;
mLookupUri = contact.getLookupUri();
final long setDataStartTime = SystemClock.elapsedRealtime();
setState(contact);
final long setDataEndTime = SystemClock.elapsedRealtime();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Time needed for setting UI: "
+ (setDataEndTime - setDataStartTime));
}
}
@Override
public void onLoaderReset(Loader<Contact> loader) {
}
};
/**
* The groups meta data loader listener.
*/
protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
new LoaderManager.LoaderCallbacks<Cursor>() {
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
GroupUtil.ALL_GROUPS_SELECTION);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mGroupMetaData = data;
setGroupMetaData();
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
};
private long mPhotoRawContactId;
private Bundle mUpdatedPhotos = new Bundle();
private InputMethodManager inputMethodManager;
@Override
public Context getContext() {
return getActivity();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity;
mEditorUtils = ContactEditorUtils.create(mContext);
mComparator = new RawContactDeltaComparator(mContext);
}
@Override
public void onCreate(Bundle savedState) {
if (savedState != null) {
// Restore mUri before calling super.onCreate so that onInitializeLoaders
// would already have a uri and an action to work with
mAction = savedState.getString(KEY_ACTION);
mLookupUri = savedState.getParcelable(KEY_URI);
}
super.onCreate(savedState);
inputMethodManager =
(InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (savedState == null) {
mViewIdGenerator = new ViewIdGenerator();
// mState can still be null because it may not have have finished loading before
// onSaveInstanceState was called.
mState = new RawContactDeltaList();
} else {
mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
KEY_RAW_CONTACTS));
// NOTE: mGroupMetaData is not saved/restored
// Read state from savedState. No loading involved here
mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
mStatus = savedState.getInt(KEY_STATUS);
mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
mEnabled = savedState.getBoolean(KEY_ENABLED);
// Aggregation PopupWindow
mAggregationSuggestionsRawContactId = savedState.getLong(
KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
// Join Activity
mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
setHasOptionsMenu(true);
final View view = inflater.inflate(
R.layout.contact_editor_fragment, container, false);
mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
validateAction(mAction);
if (mState.isEmpty()) {
// The delta list may not have finished loading before orientation change happens.
// In this case, there will be a saved state but deltas will be missing. Reload from
// database.
if (Intent.ACTION_EDIT.equals(mAction)) {
// Either
// 1) orientation change but load never finished.
// 2) not an orientation change so data needs to be loaded for first time.
getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
}
} else {
// Orientation change, we already have mState, it was loaded by onCreate
bindEditors();
}
// Handle initial actions only when existing state missing
if (savedInstanceState == null) {
if (mIntentExtras != null) {
final Account account = mIntentExtras == null ? null :
(Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
final String dataSet = mIntentExtras == null ? null :
mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
mAccountWithDataSet = account != null
? new AccountWithDataSet(account.name, account.type, dataSet)
: mIntentExtras.<AccountWithDataSet>getParcelable(
ContactEditorActivity.EXTRA_ACCOUNT_WITH_DATA_SET);
}
if (Intent.ACTION_EDIT.equals(mAction)) {
mIsEdit = true;
} else if (Intent.ACTION_INSERT.equals(mAction)) {
mHasNewContact = true;
if (mAccountWithDataSet != null) {
createContact(mAccountWithDataSet);
} // else wait for accounts to be loaded
}
}
if (mHasNewContact) {
AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter());
}
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState == null) {
return;
}
maybeRestoreFocus(savedInstanceState);
}
/**
* Checks if the requested action is valid.
*
* @param action The action to test.
* @throws IllegalArgumentException when the action is invalid.
*/
private static void validateAction(String action) {
if (VALID_INTENT_ACTIONS.contains(action)) {
return;
}
throw new IllegalArgumentException(
"Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putString(KEY_ACTION, mAction);
outState.putParcelable(KEY_URI, mLookupUri);
outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
if (mMaterialPalette != null) {
outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
}
outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
// NOTE: mGroupMetaData is not saved
outState.putParcelable(KEY_EDIT_STATE, mState);
outState.putInt(KEY_STATUS, mStatus);
outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
outState.putBoolean(KEY_IS_EDIT, mIsEdit);
outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
outState.putBoolean(KEY_ENABLED, mEnabled);
// Aggregation PopupWindow
outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
mAggregationSuggestionsRawContactId);
// Join Activity
outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
// For b/77246197
View focusedView = getView() == null ? null : getView().findFocus();
if (focusedView != null) {
outState.putInt(KEY_FOCUSED_VIEW_ID, focusedView.getId());
outState.putBoolean(KEY_RESTORE_SOFT_INPUT, inputMethodManager.isActive(focusedView));
}
super.onSaveInstanceState(outState);
}
@Override
public void onStop() {
super.onStop();
UiClosables.closeQuietly(mAggregationSuggestionPopup);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mAggregationSuggestionEngine != null) {
mAggregationSuggestionEngine.quit();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_JOIN: {
// Ignore failed requests
if (resultCode != Activity.RESULT_OK) return;
if (data != null) {
final long contactId = ContentUris.parseId(data.getData());
if (hasPendingChanges()) {
// Ask the user if they want to save changes before doing the join
JoinContactConfirmationDialogFragment.show(this, contactId);
} else {
// Do the join immediately
joinAggregate(contactId);
}
}
break;
}
case REQUEST_CODE_ACCOUNTS_CHANGED: {
// Bail if the account selector was not successful.
if (resultCode != Activity.RESULT_OK || data == null ||
!data.hasExtra(Intents.Insert.EXTRA_ACCOUNT)) {
if (mListener != null) {
mListener.onReverted();
}
return;
}
AccountWithDataSet account = data.getParcelableExtra(
Intents.Insert.EXTRA_ACCOUNT);
createContact(account);
break;
}
}
}
@Override
public void onAccountsLoaded(List<AccountInfo> data) {
mWritableAccounts = data;
// The user may need to select a new account to save to
if (mAccountWithDataSet == null && mHasNewContact) {
selectAccountAndCreateContact();
}
final RawContactEditorView view = getContent();
if (view == null) {
return;
}
view.setAccounts(data);
if (mAccountWithDataSet == null && view.getCurrentRawContactDelta() == null) {
return;
}
final AccountWithDataSet account = mAccountWithDataSet != null
? mAccountWithDataSet
: view.getCurrentRawContactDelta().getAccountWithDataSet();
// The current account was removed
if (!AccountInfo.contains(data, account) && !data.isEmpty()) {
if (isReadyToBindEditors()) {
onRebindEditorsForNewContact(getContent().getCurrentRawContactDelta(),
account, data.get(0).getAccount());
} else {
mAccountWithDataSet = data.get(0).getAccount();
}
}
}
//
// Options menu
//
@Override
public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.edit_contact, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
// This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
// because the custom action bar contains the "save" button now (not the overflow menu).
// TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
final MenuItem saveMenu = menu.findItem(R.id.menu_save);
final MenuItem splitMenu = menu.findItem(R.id.menu_split);
final MenuItem joinMenu = menu.findItem(R.id.menu_join);
final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
// TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
// on a raw contact level.
joinMenu.setVisible(false);
splitMenu.setVisible(false);
deleteMenu.setVisible(false);
// Save menu is invisible when there's only one read only contact in the editor.
saveMenu.setVisible(!isEditingReadOnlyRawContact());
if (saveMenu.isVisible()) {
// Since we're using a custom action layout we have to manually hook up the handler.
saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onOptionsItemSelected(saveMenu);
}
});
}
final MenuItem helpMenu = menu.findItem(R.id.menu_help);
helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
int size = menu.size();
for (int i = 0; i < size; i++) {
menu.getItem(i).setEnabled(mEnabled);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
return revert();
}
final Activity activity = getActivity();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
// If we no longer are attached to a running activity want to
// drain this event.
return true;
}
final int id = item.getItemId();
if (id == R.id.menu_save) {
return save(SaveMode.CLOSE);
} else if (id == R.id.menu_delete) {
if (mListener != null) mListener.onDeleteRequested(mLookupUri);
return true;
} else if (id == R.id.menu_split) {
return doSplitContactAction();
} else if (id == R.id.menu_join) {
return doJoinContactAction();
} else if (id == R.id.menu_help) {
HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
return true;
}
return false;
}
@Override
public boolean revert() {
if (mState.isEmpty() || !hasPendingChanges()) {
onCancelEditConfirmed();
} else {
CancelEditDialogFragment.show(this);
}
return true;
}
@Override
public void onCancelEditConfirmed() {
// When this Fragment is closed we don't want it to auto-save
mStatus = Status.CLOSING;
if (mListener != null) {
mListener.onReverted();
}
}
@Override
public void onSplitContactConfirmed(boolean hasPendingChanges) {
if (mState.isEmpty()) {
// This may happen when this Fragment is recreated by the system during users
// confirming the split action (and thus this method is called just before onCreate()),
// for example.
Log.e(TAG, "mState became null during the user's confirming split action. " +
"Cannot perform the save action.");
return;
}
if (!hasPendingChanges && mHasNewContact) {
// If the user didn't add anything new, we don't want to split out the newly created
// raw contact into a name-only contact so remove them.
final Iterator<RawContactDelta> iterator = mState.iterator();
while (iterator.hasNext()) {
final RawContactDelta rawContactDelta = iterator.next();
if (rawContactDelta.getRawContactId() < 0) {
iterator.remove();
}
}
}
mState.markRawContactsForSplitting();
save(SaveMode.SPLIT);
}
@Override
public void onSplitContactCanceled() {}
private boolean doSplitContactAction() {
if (!hasValidState()) return false;
SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
return true;
}
private boolean doJoinContactAction() {
if (!hasValidState() || mLookupUri == null) {
return false;
}
// If we just started creating a new contact and haven't added any data, it's too
// early to do a join
if (mState.size() == 1 && mState.get(0).isContactInsert()
&& !hasPendingChanges()) {
Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
Toast.LENGTH_LONG).show();
return true;
}
showJoinAggregateActivity(mLookupUri);
return true;
}
@Override
public void onJoinContactConfirmed(long joinContactId) {
doSaveAction(SaveMode.JOIN, joinContactId);
}
@Override
public boolean save(int saveMode) {
if (!hasValidState() || mStatus != Status.EDITING) {
return false;
}
// If we are about to close the editor - there is no need to refresh the data
if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
|| saveMode == SaveMode.SPLIT) {
getLoaderManager().destroyLoader(LOADER_CONTACT);
}
mStatus = Status.SAVING;
if (!hasPendingChanges()) {
if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
// We don't have anything to save and there isn't even an existing contact yet.
// Nothing to do, simply go back to editing mode
mStatus = Status.EDITING;
return true;
}
onSaveCompleted(/* hadChanges =*/ false, saveMode,
/* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
return true;
}
setEnabled(false);
hideSoftKeyboard();
return doSaveAction(saveMode, /* joinContactId */ null);
}
//
// State accessor methods
//
/**
* Check if our internal {@link #mState} is valid, usually checked before
* performing user actions.
*/
private boolean hasValidState() {
return mState.size() > 0;
}
private boolean isEditingUserProfile() {
return mNewLocalProfile || mIsUserProfile;
}
/**
* Whether the contact being edited is composed of read-only raw contacts
* aggregated with a newly created writable raw contact.
*/
private boolean isEditingReadOnlyRawContactWithNewContact() {
return mHasNewContact && mState.size() > 1;
}
/**
* @return true if the single raw contact we're looking at is read-only.
*/
private boolean isEditingReadOnlyRawContact() {
return hasValidState() && mRawContactIdToDisplayAlone > 0
&& !mState.getByRawContactId(mRawContactIdToDisplayAlone)
.getAccountType(AccountTypeManager.getInstance(mContext))
.areContactsWritable();
}
/**
* Return true if there are any edits to the current contact which need to
* be saved.
*/
private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
}
/**
* Determines if changes were made in the editor that need to be saved, while taking into
* account that name changes are not real for read-only contacts.
* See go/editing-read-only-contacts
*/
private boolean hasPendingChanges() {
if (isEditingReadOnlyRawContactWithNewContact()) {
// We created a new raw contact delta with a default display name.
// We must test for pending changes while ignoring the default display name.
final RawContactDelta beforeRawContactDelta = mState
.getByRawContactId(mReadOnlyDisplayNameId);
final ValuesDelta beforeDelta = beforeRawContactDelta == null ? null :
beforeRawContactDelta.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
final ValuesDelta pendingDelta = mState
.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
final Set<String> excludedMimeTypes = new HashSet<>();
excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
return hasPendingRawContactChanges(excludedMimeTypes);
}
return true;
}
return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
}
/**
* Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
* of a read only delta and now we want to check if the copied delta has changes.
*
* @param before original {@link ValuesDelta}
* @param after copied {@link ValuesDelta}
* @return true if the copied {@link ValuesDelta} has all the same values in the structured
* name fields as the original.
*/
private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
if (before == after) return true;
if (before == null || after == null) return false;
final ContentValues original = before.getBefore();
final ContentValues pending = after.getAfter();
if (original != null && pending != null) {
final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
final String beforePrefix = original.getAsString(StructuredName.PREFIX);
final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
return TextUtils.equals(beforeSuffix, afterSuffix);
}
return false;
}
//
// Account creation
//
private void selectAccountAndCreateContact() {
Preconditions.checkNotNull(mWritableAccounts, "Accounts must be loaded first");
// If this is a local profile, then skip the logic about showing the accounts changed
// activity and create a phone-local contact.
if (mNewLocalProfile) {
createContact(null);
return;
}
final List<AccountWithDataSet> accounts = AccountInfo.extractAccounts(mWritableAccounts);
// If there is no default account or the accounts have changed such that we need to
// prompt the user again, then launch the account prompt.
if (mEditorUtils.shouldShowAccountChangedNotification(accounts)) {
Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
// Prevent a second instance from being started on rotates
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
mStatus = Status.SUB_ACTIVITY;
startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
} else {
// Make sure the default account is automatically set if there is only one non-device
// account.
mEditorUtils.maybeUpdateDefaultAccount(accounts);
// Otherwise, there should be a default account. Then either create a local contact
// (if default account is null) or create a contact with the specified account.
AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(accounts);
createContact(defaultAccount);
}
}
/**
* Shows account creation screen associated with a given account.
*
* @param account may be null to signal a device-local contact should be created.
*/
private void createContact(AccountWithDataSet account) {
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
setStateForNewContact(account, accountType, isEditingUserProfile());
}
//
// Data binding
//
private void setState(Contact contact) {
// If we have already loaded data, we do not want to change it here to not confuse the user
if (!mState.isEmpty()) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Ignoring background change. This will have to be rebased later");
}
return;
}
mContact = contact;
mRawContacts = contact.getRawContacts();
// Check for writable raw contacts. If there are none, then we need to create one so user
// can edit. For the user profile case, there is already an editable contact.
if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
mHasNewContact = true;
mReadOnlyDisplayNameId = contact.getNameRawContactId();
mCopyReadOnlyName = true;
// This is potentially an asynchronous call and will add deltas to list.
selectAccountAndCreateContact();
} else {
mHasNewContact = false;
}
setStateForExistingContact(contact.isUserProfile(), mRawContacts);
if (mAutoAddToDefaultGroup
&& InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
}
}
/**
* Prepare {@link #mState} for a newly created phone-local contact.
*/
private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
boolean isUserProfile) {
setStateForNewContact(account, accountType, /* oldState =*/ null,
/* oldAccountType =*/ null, isUserProfile);
}
/**
* Prepare {@link #mState} for a newly created phone-local contact, migrating the state
* specified by oldState and oldAccountType.
*/
private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
mStatus = Status.EDITING;
mAccountWithDataSet = account;
mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
mIsUserProfile = isUserProfile;
mNewContactDataReady = true;
bindEditors();
}
/**
* Returns a {@link RawContactDelta} for a new contact suitable for addition into
* {@link #mState}.
*
* If oldState and oldAccountType are specified, the state specified by those parameters
* is migrated to the result {@link RawContactDelta}.
*/
private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
final RawContact rawContact = new RawContact();
if (account != null) {
rawContact.setAccount(account);
} else {
rawContact.setAccountToLocal();
}
final RawContactDelta result = new RawContactDelta(
ValuesDelta.fromAfter(rawContact.getValues()));
if (oldState == null) {
// Parse any values from incoming intent
RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
} else {
RawContactModifier.migrateStateForNewContact(
mContext, oldState, result, oldAccountType, accountType);
}
// Ensure we have some default fields (if the account type does not support a field,
// ensureKind will not add it, so it is safe to add e.g. Event)
RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
RawContactModifier.ensureKindExists(result, accountType,
StructuredPostal.CONTENT_ITEM_TYPE);
// Set the correct URI for saving the contact as a profile
if (mNewLocalProfile) {
result.setProfileQueryUri();
}
return result;
}
/**
* Prepare {@link #mState} for an existing contact.
*/
private void setStateForExistingContact(boolean isUserProfile,
ImmutableList<RawContact> rawContacts) {
setEnabled(true);
mState.addAll(rawContacts.iterator());
setIntentExtras(mIntentExtras);
mIntentExtras = null;
// For user profile, change the contacts query URI
mIsUserProfile = isUserProfile;
boolean localProfileExists = false;
if (mIsUserProfile) {
for (RawContactDelta rawContactDelta : mState) {
// For profile contacts, we need a different query URI
rawContactDelta.setProfileQueryUri();
// Try to find a local profile contact
if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
localProfileExists = true;
}
}
// Editor should always present a local profile for editing
// TODO(wjang): Need to figure out when this case comes up. We can't do this if we're
// going to prune all but the one raw contact that we're trying to display by itself.
if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
mState.add(createLocalRawContactDelta());
}
}
mExistingContactDataReady = true;
bindEditors();
}
/**
* Set the enabled state of editors.
*/
private void setEnabled(boolean enabled) {
if (mEnabled != enabled) {
mEnabled = enabled;
// Enable/disable editors
if (mContent != null) {
int count = mContent.getChildCount();
for (int i = 0; i < count; i++) {
mContent.getChildAt(i).setEnabled(enabled);
}
}
// Maybe invalidate the options menu
final Activity activity = getActivity();
if (activity != null) activity.invalidateOptionsMenu();
}
}
/**
* Returns a {@link RawContactDelta} for a local contact suitable for addition into
* {@link #mState}.
*/
private static RawContactDelta createLocalRawContactDelta() {
final RawContact rawContact = new RawContact();
rawContact.setAccountToLocal();
final RawContactDelta result = new RawContactDelta(
ValuesDelta.fromAfter(rawContact.getValues()));
result.setProfileQueryUri();
return result;
}
private void copyReadOnlyName() {
// We should only ever be doing this if we're creating a new writable contact to attach to
// a read only contact.
if (!isEditingReadOnlyRawContactWithNewContact()) {
return;
}
final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
final RawContactDelta writable = mState.get(writableIndex);
final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
final ValuesDelta writeNameDelta = writable
.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
final ValuesDelta readNameDelta = readOnly
.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
mCopyReadOnlyName = false;
if (writeNameDelta == null || readNameDelta == null) {
return;
}
writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
}
/**
* Bind editors using {@link #mState} and other members initialized from the loaded (or new)
* Contact.
*/
protected void bindEditors() {
if (!isReadyToBindEditors()) {
return;
}
// Add input fields for the loaded Contact
final RawContactEditorView editorView = getContent();
editorView.setListener(this);
if (mCopyReadOnlyName) {
copyReadOnlyName();
}
editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
mHasNewContact, mIsUserProfile, mAccountWithDataSet,
mRawContactIdToDisplayAlone);
if (isEditingReadOnlyRawContact()) {
final Toolbar toolbar = getEditorActivity().getToolbar();
if (toolbar != null) {
toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
// Set activity title for Talkback
getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
toolbar.getNavigationIcon().setAutoMirrored(true);
}
}
// Set up the photo widget
editorView.setPhotoListener(this);
mPhotoRawContactId = editorView.getPhotoRawContactId();
// If there is an updated full resolution photo apply it now, this will be the case if
// the user selects or takes a new photo, then rotates the device.
final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
if (uri != null) {
editorView.setFullSizePhoto(uri);
}
final StructuredNameEditorView nameEditor = editorView.getNameEditorView();
final TextFieldsEditorView phoneticNameEditor = editorView.getPhoneticEditorView();
final boolean useJapaneseOrder =
Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
if (useJapaneseOrder && nameEditor != null && phoneticNameEditor != null) {
nameEditor.setPhoneticView(phoneticNameEditor);
}
// The editor is ready now so make it visible
editorView.setEnabled(mEnabled);
editorView.setVisibility(View.VISIBLE);
// Refresh the ActionBar as the visibility of the join command
// Activity can be null if we have been detached from the Activity.
invalidateOptionsMenu();
}
/**
* Invalidates the options menu if we are still associated with an Activity.
*/
private void invalidateOptionsMenu() {
final Activity activity = getActivity();
if (activity != null) {
activity.invalidateOptionsMenu();
}
}
private boolean isReadyToBindEditors() {
if (mState.isEmpty()) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "No data to bind editors");
}
return false;
}
if (mIsEdit && !mExistingContactDataReady) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Existing contact data is not ready to bind editors.");
}
return false;
}
if (mHasNewContact && !mNewContactDataReady) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "New contact data is not ready to bind editors.");
}
return false;
}
// Don't attempt to bind anything if we have no permissions.
return RequestPermissionsActivity.hasRequiredPermissions(mContext);
}
/**
* Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
* Some of old data are reused with new restriction enforced by the new account.
*
* @param oldState Old data being edited.
* @param oldAccount Old account associated with oldState.
* @param newAccount New account to be used.
*/
private void rebindEditorsForNewContact(
RawContactDelta oldState, AccountWithDataSet oldAccount,
AccountWithDataSet newAccount) {
AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
mExistingContactDataReady = false;
mNewContactDataReady = false;
mState = new RawContactDeltaList();
setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
isEditingUserProfile());
if (mIsEdit) {
setStateForExistingContact(isEditingUserProfile(), mRawContacts);
}
}
//
// ContactEditor
//
@Override
public void setListener(Listener listener) {
mListener = listener;
}
@Override
public void load(String action, Uri lookupUri, Bundle intentExtras) {
mAction = action;
mLookupUri = lookupUri;
mIntentExtras = intentExtras;
if (mIntentExtras != null) {
mAutoAddToDefaultGroup =
mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
mNewLocalProfile =
mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
mDisableDeleteMenuOption =
mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
&& mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
}
mRawContactIdToDisplayAlone = mIntentExtras
.getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
}
}
@Override
public void setIntentExtras(Bundle extras) {
getContent().setIntentExtras(extras);
}
@Override
public void onJoinCompleted(Uri uri) {
onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
}
private String getNameToDisplay(Uri contactUri) {
// The contact has been deleted or the uri is otherwise no longer right.
if (contactUri == null) {
return null;
}
final ContentResolver resolver = mContext.getContentResolver();
final Cursor cursor = resolver.query(contactUri, new String[]{
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
final String displayName = cursor.getString(0);
final String displayNameAlt = cursor.getString(1);
cursor.close();
return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
new ContactsPreferences(mContext));
}
} finally {
cursor.close();
}
}
return null;
}
@Override
public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
Uri contactLookupUri, Long joinContactId) {
if (hadChanges) {
if (saveSucceeded) {
switch (saveMode) {
case SaveMode.JOIN:
break;
case SaveMode.SPLIT:
Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
.show();
break;
default:
final String displayName = getNameToDisplay(contactLookupUri);
final String toastMessage;
if (!TextUtils.isEmpty(displayName)) {
toastMessage = getResources().getString(
R.string.contactSavedNamedToast, displayName);
} else {
toastMessage = getResources().getString(R.string.contactSavedToast);
}
Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
}
}
switch (saveMode) {
case SaveMode.CLOSE: {
final Intent resultIntent;
if (saveSucceeded && contactLookupUri != null) {
final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
mContext, contactLookupUri, mLookupUri);
resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
mContext, lookupUri, ScreenType.EDITOR);
resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
} else {
resultIntent = null;
}
// It is already saved, so prevent it from being saved again
mStatus = Status.CLOSING;
if (mListener != null) mListener.onSaveFinished(resultIntent);
break;
}
case SaveMode.EDITOR: {
// It is already saved, so prevent it from being saved again
mStatus = Status.CLOSING;
if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
break;
}
case SaveMode.JOIN:
if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
joinAggregate(joinContactId);
}
break;
case SaveMode.RELOAD:
if (saveSucceeded && contactLookupUri != null) {
// If this was in INSERT, we are changing into an EDIT now.
// If it already was an EDIT, we are changing to the new Uri now
mState = new RawContactDeltaList();
load(Intent.ACTION_EDIT, contactLookupUri, null);
mStatus = Status.LOADING;
getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
}
break;
case SaveMode.SPLIT:
mStatus = Status.CLOSING;
if (mListener != null) {
mListener.onContactSplit(contactLookupUri);
} else if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "No listener registered, can not call onSplitFinished");
}
break;
}
}
/**
* Shows a list of aggregates that can be joined into the currently viewed aggregate.
*
* @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
*/
private void showJoinAggregateActivity(Uri contactLookupUri) {
if (contactLookupUri == null || !isAdded()) {
return;
}
mContactIdForJoin = ContentUris.parseId(contactLookupUri);
final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
startActivityForResult(intent, REQUEST_CODE_JOIN);
}
//
// Aggregation PopupWindow
//
/**
* Triggers an asynchronous search for aggregation suggestions.
*/
protected void acquireAggregationSuggestions(Context context,
long rawContactId, ValuesDelta valuesDelta) {
mAggregationSuggestionsRawContactId = rawContactId;
if (mAggregationSuggestionEngine == null) {
mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
mAggregationSuggestionEngine.setListener(this);
mAggregationSuggestionEngine.start();
}
mAggregationSuggestionEngine.setContactId(getContactId());
mAggregationSuggestionEngine.setAccountFilter(
getContent().getCurrentRawContactDelta().getAccountWithDataSet());
mAggregationSuggestionEngine.onNameChange(valuesDelta);
}
/**
* Returns the contact ID for the currently edited contact or 0 if the contact is new.
*/
private long getContactId() {
for (RawContactDelta rawContact : mState) {
Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
if (contactId != null) {
return contactId;
}
}
return 0;
}
@Override
public void onAggregationSuggestionChange() {
final Activity activity = getActivity();
if ((activity != null && activity.isFinishing())
|| !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) {
return;
}
UiClosables.closeQuietly(mAggregationSuggestionPopup);
if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
return;
}
final View anchorView = getAggregationAnchorView();
if (anchorView == null) {
return; // Raw contact deleted?
}
mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
mAggregationSuggestionPopup.setAnchorView(anchorView);
mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
mAggregationSuggestionPopup.setAdapter(
new AggregationSuggestionAdapter(
getActivity(),
/* listener =*/ this,
mAggregationSuggestionEngine.getSuggestions()));
mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
suggestionView.handleItemClickEvent();
UiClosables.closeQuietly(mAggregationSuggestionPopup);
mAggregationSuggestionPopup = null;
}
});
mAggregationSuggestionPopup.show();
}
/**
* Returns the editor view that should be used as the anchor for aggregation suggestions.
*/
protected View getAggregationAnchorView() {
return getContent().getAggregationAnchorView();
}
/**
* Joins the suggested contact (specified by the id's of constituent raw
* contacts), save all changes, and stay in the editor.
*/
public void doJoinSuggestedContact(long[] rawContactIds) {
if (!hasValidState() || mStatus != Status.EDITING) {
return;
}
mState.setJoinWithRawContacts(rawContactIds);
save(SaveMode.RELOAD);
}
@Override
public void onEditAction(Uri contactLookupUri, long rawContactId) {
SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
}
/**
* Abandons the currently edited contact and switches to editing the selected raw contact,
* transferring all the data there
*/
public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
if (mListener != null) {
// make sure we don't save this contact when closing down
mStatus = Status.CLOSING;
mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
getContent().getCurrentRawContactDelta().getContentValues());
}
}
/**
* Sets group metadata on all bound editors.
*/
protected void setGroupMetaData() {
if (mGroupMetaData != null) {
getContent().setGroupMetaData(mGroupMetaData);
}
}
/**
* Persist the accumulated editor deltas.
*
* @param joinContactId the raw contact ID to join the contact being saved to after the save,
* may be null.
*/
protected boolean doSaveAction(int saveMode, Long joinContactId) {
final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
((Activity) mContext).getClass(),
ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
return startSaveService(mContext, intent, saveMode);
}
private boolean startSaveService(Context context, Intent intent, int saveMode) {
final boolean result = ContactSaveService.startService(
context, intent, saveMode);
if (!result) {
onCancelEditConfirmed();
}
return result;
}
//
// Join Activity
//
/**
* Performs aggregation with the contact selected by the user from suggestions or A-Z list.
*/
protected void joinAggregate(final long contactId) {
final Intent intent = ContactSaveService.createJoinContactsIntent(
mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
ContactEditorActivity.ACTION_JOIN_COMPLETED);
mContext.startService(intent);
}
public void removePhoto() {
getContent().removePhoto();
mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
}
public void updatePhoto(Uri uri) throws FileNotFoundException {
final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
Toast.LENGTH_SHORT).show();
return;
}
mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
getContent().updatePhoto(uri);
}
public void setPrimaryPhoto() {
getContent().setPrimaryPhoto();
}
@Override
public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
final Activity activity = getActivity();
if (activity == null || activity.isFinishing()) {
return;
}
acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
}
@Override
public void onRebindEditorsForNewContact(RawContactDelta oldState,
AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
mNewContactAccountChanged = true;
rebindEditorsForNewContact(oldState, oldAccount, newAccount);
}
@Override
public void onBindEditorsFailed() {
final Activity activity = getActivity();
if (activity != null && !activity.isFinishing()) {
Toast.makeText(activity, R.string.editor_failed_to_load,
Toast.LENGTH_SHORT).show();
activity.setResult(Activity.RESULT_CANCELED);
activity.finish();
}
}
@Override
public void onEditorsBound() {
final Activity activity = getActivity();
if (activity == null || activity.isFinishing()) {
return;
}
getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
}
@Override
public void onPhotoEditorViewClicked() {
// For contacts composed of a single writable raw contact, or raw contacts have no more
// than 1 photo, clicking the photo view simply opens the source photo dialog
getEditorActivity().changePhoto(getPhotoMode());
}
private int getPhotoMode() {
return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
: PhotoActionPopup.Modes.NO_PHOTO;
}
private ContactEditorActivity getEditorActivity() {
return (ContactEditorActivity) getActivity();
}
private RawContactEditorView getContent() {
return (RawContactEditorView) mContent;
}
// TODO(b/77246197): figure out a better way to address focus being lost on rotation.
private void maybeRestoreFocus(Bundle savedInstanceState) {
int focusedViewId = savedInstanceState.getInt(KEY_FOCUSED_VIEW_ID, View.NO_ID);
if (focusedViewId == View.NO_ID) {
return;
}
boolean shouldRestoreSoftInput = savedInstanceState.getBoolean(KEY_RESTORE_SOFT_INPUT);
new Handler()
.postDelayed(
() -> {
if (!isResumed()) {
return;
}
View root = getView();
if (root == null) {
return;
}
View focusedView = root.findFocus();
if (focusedView != null) {
return;
}
focusedView = getView().findViewById(focusedViewId);
if (focusedView == null) {
return;
}
boolean didFocus = focusedView.requestFocus();
if (!didFocus) {
Log.i(TAG, "requestFocus failed");
return;
}
if (shouldRestoreSoftInput) {
boolean didShow = inputMethodManager
.showSoftInput(focusedView, InputMethodManager.SHOW_IMPLICIT);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "showSoftInput -> " + didShow);
}
}
},
RESTORE_FOCUS_DELAY_MILLIS);
}
private void hideSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
Context.INPUT_METHOD_SERVICE);
if (imm != null && mContent != null) {
imm.hideSoftInputFromWindow(
mContent.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
}