/* * 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.activities; import android.app.Dialog; import android.app.FragmentTransaction; import android.content.ComponentName; import android.content.ContentValues; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.QuickContact; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; import com.android.contacts.AppCompatContactsActivity; import com.android.contacts.ContactSaveService; import com.android.contacts.DynamicShortcuts; import com.android.contacts.R; import com.android.contacts.detail.PhotoSelectionHandler; import com.android.contacts.editor.ContactEditorFragment; import com.android.contacts.editor.EditorIntents; import com.android.contacts.editor.PhotoSourceDialogFragment; import com.android.contacts.interactions.ContactDeletionInteraction; import com.android.contacts.model.RawContactDeltaList; import com.android.contacts.util.DialogManager; import com.android.contacts.util.ImplicitIntentsUtil; import java.io.FileNotFoundException; import java.util.ArrayList; /** * Contact editor with only the most important fields displayed initially. */ public class ContactEditorActivity extends AppCompatContactsActivity implements PhotoSourceDialogFragment.Listener, DialogManager.DialogShowingViewActivity { private static final String TAG = "ContactEditorActivity"; public static final String ACTION_JOIN_COMPLETED = "joinCompleted"; public static final String ACTION_SAVE_COMPLETED = "saveCompleted"; public static final int RESULT_CODE_SPLIT = 2; // 3 used for ContactDeletionInteraction.RESULT_CODE_DELETED public static final int RESULT_CODE_EDITED = 4; /** * The contact will be saved to this account when this is set for an insert. This * is necessary because {@link android.accounts.Account} cannot be created with null values * for the name and type and an Account is needed for * {@link android.provider.ContactsContract.Intents.Insert#EXTRA_ACCOUNT} */ public static final String EXTRA_ACCOUNT_WITH_DATA_SET = "com.android.contacts.ACCOUNT_WITH_DATA_SET"; private static final String TAG_EDITOR_FRAGMENT = "editor_fragment"; private static final String STATE_PHOTO_MODE = "photo_mode"; private static final String STATE_ACTION_BAR_TITLE = "action_bar_title"; private static final String STATE_PHOTO_URI = "photo_uri"; /** * Boolean intent key that specifies that this activity should finish itself * (instead of launching a new view intent) after the editor changes have been * saved. */ public static final String INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED = "finishActivityOnSaveCompleted"; /** * Contract for contact editors Fragments that are managed by this Activity. */ public interface ContactEditor { /** * Modes that specify what the AsyncTask has to perform after saving */ interface SaveMode { /** * Close the editor after saving */ int CLOSE = 0; /** * Reload the data so that the user can continue editing */ int RELOAD = 1; /** * Split the contact after saving */ int SPLIT = 2; /** * Join another contact after saving */ int JOIN = 3; /** * Navigate to the editor view after saving. */ int EDITOR = 4; } /** * The status of the contact editor. */ interface Status { /** * The loader is fetching data */ int LOADING = 0; /** * Not currently busy. We are waiting for the user to enter data */ int EDITING = 1; /** * The data is currently being saved. This is used to prevent more * auto-saves (they shouldn't overlap) */ int SAVING = 2; /** * Prevents any more saves. This is used if in the following cases: * - After Save/Close * - After Revert * - After the user has accepted an edit suggestion * - After the user chooses to expand the editor */ int CLOSING = 3; /** * Prevents saving while running a child activity. */ int SUB_ACTIVITY = 4; } /** * Sets the hosting Activity that will receive callbacks from the contact editor. */ void setListener(ContactEditorFragment.Listener listener); /** * Initialize the contact editor. */ void load(String action, Uri lookupUri, Bundle intentExtras); /** * Applies extras from the hosting Activity to the writable raw contact. */ void setIntentExtras(Bundle extras); /** * Saves or creates the contact based on the mode, and if successful * finishes the activity. */ boolean save(int saveMode); /** * If there are no unsaved changes, just close the editor, otherwise the user is prompted * before discarding unsaved changes. */ boolean revert(); /** * Invoked after the contact is saved. */ void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, Uri contactLookupUri, Long joinContactId); /** * Invoked after the contact is joined. */ void onJoinCompleted(Uri uri); } /** * Displays a PopupWindow with photo edit options. */ private final class EditorPhotoSelectionHandler extends PhotoSelectionHandler { /** * Receiver of photo edit option callbacks. */ private final class EditorPhotoActionListener extends PhotoActionListener { @Override public void onRemovePictureChosen() { getEditorFragment().removePhoto(); } @Override public void onPhotoSelected(Uri uri) throws FileNotFoundException { mPhotoUri = uri; getEditorFragment().updatePhoto(uri); // Re-create the photo handler the next time we need it so that additional photo // selections create a new temp file (and don't hit the one that was just added // to the cache). mPhotoSelectionHandler = null; } @Override public Uri getCurrentPhotoUri() { return mPhotoUri; } @Override public void onPhotoSelectionDismissed() { } } private final EditorPhotoActionListener mPhotoActionListener; public EditorPhotoSelectionHandler(int photoMode) { // We pass a null changeAnchorView since we are overriding onClick so that we // can show the photo options in a dialog instead of a ListPopupWindow (which would // be anchored at changeAnchorView). // TODO: empty raw contact delta list super(ContactEditorActivity.this, /* changeAnchorView =*/ null, photoMode, /* isDirectoryContact =*/ false, new RawContactDeltaList()); mPhotoActionListener = new EditorPhotoActionListener(); } @Override public PhotoActionListener getListener() { return mPhotoActionListener; } @Override protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { mPhotoUri = photoUri; startActivityForResult(intent, requestCode); } } private int mActionBarTitleResId; private ContactEditor mFragment; private Toolbar mToolbar; private boolean mFinishActivityOnSaveCompleted; private DialogManager mDialogManager = new DialogManager(this); private EditorPhotoSelectionHandler mPhotoSelectionHandler; private Uri mPhotoUri; private int mPhotoMode; private final ContactEditorFragment.Listener mFragmentListener = new ContactEditorFragment.Listener() { @Override public void onDeleteRequested(Uri contactUri) { ContactDeletionInteraction.start( ContactEditorActivity.this, contactUri, true); } @Override public void onReverted() { finish(); } @Override public void onSaveFinished(Intent resultIntent) { if (mFinishActivityOnSaveCompleted) { setResult(resultIntent == null ? RESULT_CANCELED : RESULT_OK, resultIntent); } else if (resultIntent != null) { ImplicitIntentsUtil.startActivityInApp( ContactEditorActivity.this, resultIntent); } finish(); } @Override public void onContactSplit(Uri newLookupUri) { setResult(RESULT_CODE_SPLIT, /* data */ null); finish(); } @Override public void onContactNotFound() { finish(); } @Override public void onEditOtherRawContactRequested( Uri contactLookupUri, long rawContactId, ArrayList values) { final Intent intent = EditorIntents.createEditOtherRawContactIntent( ContactEditorActivity.this, contactLookupUri, rawContactId, values); ImplicitIntentsUtil.startActivityInApp( ContactEditorActivity.this, intent); finish(); } }; @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); RequestPermissionsActivity.startPermissionActivityIfNeeded(this); final Intent intent = getIntent(); final String action = intent.getAction(); // Update the component name of our intent to be this class to clear out any activity // aliases. Otherwise ContactSaveService won't notify this activity once a save is finished. // See b/34154706 for more info. intent.setComponent(new ComponentName(this, ContactEditorActivity.class)); // Determine whether or not this activity should be finished after the user is done // editing the contact or if this activity should launch another activity to view the // contact's details. mFinishActivityOnSaveCompleted = intent.getBooleanExtra( INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, false); // The only situation where action could be ACTION_JOIN_COMPLETED is if the // user joined the contact with another and closed the activity before // the save operation was completed. The activity should remain closed then. if (ACTION_JOIN_COMPLETED.equals(action)) { finish(); return; } if (ACTION_SAVE_COMPLETED.equals(action)) { finish(); return; } setContentView(R.layout.contact_editor_activity); mToolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(mToolbar); if (Intent.ACTION_EDIT.equals(action)) { mActionBarTitleResId = R.string.contact_editor_title_existing_contact; } else { mActionBarTitleResId = R.string.contact_editor_title_new_contact; } mToolbar.setTitle(mActionBarTitleResId); // Set activity title for Talkback setTitle(mActionBarTitleResId); if (savedState == null) { // Create the editor and photo selection fragments mFragment = new ContactEditorFragment(); getFragmentManager().beginTransaction() .add(R.id.fragment_container, getEditorFragment(), TAG_EDITOR_FRAGMENT) .commit(); } else { // Restore state mPhotoMode = savedState.getInt(STATE_PHOTO_MODE); mActionBarTitleResId = savedState.getInt(STATE_ACTION_BAR_TITLE); mPhotoUri = Uri.parse(savedState.getString(STATE_PHOTO_URI)); // Show/hide the editor and photo selection fragments (w/o animations) mFragment = (ContactEditorFragment) getFragmentManager() .findFragmentByTag(TAG_EDITOR_FRAGMENT); final FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); fragmentTransaction.show(getEditorFragment()).commit(); mToolbar.setTitle(mActionBarTitleResId); } // Set listeners mFragment.setListener(mFragmentListener); // Load editor data (even if it's hidden) final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null; mFragment.load(action, uri, getIntent().getExtras()); if (Intent.ACTION_INSERT.equals(action)) { DynamicShortcuts.reportShortcutUsed(this, DynamicShortcuts.SHORTCUT_ADD_CONTACT); } } @Override protected void onPause() { super.onPause(); final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); final View currentFocus = getCurrentFocus(); if (imm != null && currentFocus != null) { imm.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (mFragment == null) { return; } final String action = intent.getAction(); if (Intent.ACTION_EDIT.equals(action)) { mFragment.setIntentExtras(intent.getExtras()); } else if (ACTION_SAVE_COMPLETED.equals(action)) { mFragment.onSaveCompleted(true, intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY, ContactEditor.SaveMode.CLOSE), intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false), intent.getData(), intent.getLongExtra(ContactEditorFragment.JOIN_CONTACT_ID_EXTRA_KEY, -1)); } else if (ACTION_JOIN_COMPLETED.equals(action)) { mFragment.onJoinCompleted(intent.getData()); } } @Override protected Dialog onCreateDialog(int id, Bundle args) { if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args); // Nobody knows about the Dialog Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args); return null; } @Override public DialogManager getDialogManager() { return mDialogManager; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(STATE_PHOTO_MODE, mPhotoMode); outState.putInt(STATE_ACTION_BAR_TITLE, mActionBarTitleResId); outState.putString(STATE_PHOTO_URI, mPhotoUri != null ? mPhotoUri.toString() : Uri.EMPTY.toString()); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (mPhotoSelectionHandler == null) { mPhotoSelectionHandler = (EditorPhotoSelectionHandler) getPhotoSelectionHandler(); } if (mPhotoSelectionHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { return; } super.onActivityResult(requestCode, resultCode, data); } @Override public void onBackPressed() { if (mFragment != null) { mFragment.revert(); } } /** * Opens a dialog showing options for the user to change their photo (take, choose, or remove * photo). */ public void changePhoto(int photoMode) { mPhotoMode = photoMode; // This method is called from an onClick handler in the PhotoEditorView. It's possible for // onClick methods to run after onSaveInstanceState is called for the activity, so check // if it's safe to commit transactions before trying. if (isSafeToCommitTransactions()) { PhotoSourceDialogFragment.show(this, mPhotoMode); } } public Toolbar getToolbar() { return mToolbar; } @Override public void onRemovePictureChosen() { getPhotoSelectionHandler().getListener().onRemovePictureChosen(); } @Override public void onTakePhotoChosen() { getPhotoSelectionHandler().getListener().onTakePhotoChosen(); } @Override public void onPickFromGalleryChosen() { getPhotoSelectionHandler().getListener().onPickFromGalleryChosen(); } private PhotoSelectionHandler getPhotoSelectionHandler() { if (mPhotoSelectionHandler == null) { mPhotoSelectionHandler = new EditorPhotoSelectionHandler(mPhotoMode); } return mPhotoSelectionHandler; } private ContactEditorFragment getEditorFragment() { return (ContactEditorFragment) mFragment; } }