/* * Copyright (C) 2009 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 cn.edu.tsinghua.hpc.tcontacts.ui; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import android.accounts.Account; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ActivityNotFoundException; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Entity; import android.content.Intent; import android.content.OperationApplicationException; import android.content.ContentProviderOperation.Builder; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.AggregationExceptions; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.Contacts.Data; import android.util.Log; import android.view.ContextThemeWrapper; 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.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.TextView; import android.widget.Toast; import cn.edu.tsinghua.hpc.tcontacts.ContactsListActivity; import cn.edu.tsinghua.hpc.tcontacts.ContactsUtils; import cn.edu.tsinghua.hpc.tcontacts.R; import cn.edu.tsinghua.hpc.tcontacts.model.ContactsSource; import cn.edu.tsinghua.hpc.tcontacts.model.Editor; import cn.edu.tsinghua.hpc.tcontacts.model.EntityDelta; import cn.edu.tsinghua.hpc.tcontacts.model.EntityModifier; import cn.edu.tsinghua.hpc.tcontacts.model.EntitySet; import cn.edu.tsinghua.hpc.tcontacts.model.GoogleSource; import cn.edu.tsinghua.hpc.tcontacts.model.Sources; import cn.edu.tsinghua.hpc.tcontacts.model.ContactsSource.EditType; import cn.edu.tsinghua.hpc.tcontacts.model.Editor.EditorListener; import cn.edu.tsinghua.hpc.tcontacts.model.EntityDelta.ValuesDelta; import cn.edu.tsinghua.hpc.tcontacts.syncaction.ContactsUtility; import cn.edu.tsinghua.hpc.tcontacts.syncaction.SyncState; import cn.edu.tsinghua.hpc.tcontacts.ui.widget.BaseContactEditorView; import cn.edu.tsinghua.hpc.tcontacts.ui.widget.PhotoEditorView; import cn.edu.tsinghua.hpc.tcontacts.util.EmptyService; import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract; import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TAggregationExceptions; import cn.edu.tsinghua.hpc.tcontacts.util.TIntent; import cn.edu.tsinghua.hpc.tcontacts.util.WeakAsyncTask; import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TContacts; import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TContentProviderOperation; import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TRawContacts; import com.google.android.collect.Lists; /** * Activity for editing or inserting a contact. */ public final class EditContactActivity extends Activity implements View.OnClickListener, Comparator<EntityDelta> { private static final String TAG = "EditContactActivity"; /** The launch code when picking a photo and the raw data is returned */ private static final int PHOTO_PICKED_WITH_DATA = 3021; /** The launch code when a contact to join with is returned */ private static final int REQUEST_JOIN_CONTACT = 3022; private static final String KEY_EDIT_STATE = "state"; private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; /** The result code when view activity should close after edit returns */ public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777; public static final int SAVE_MODE_DEFAULT = 0; public static final int SAVE_MODE_SPLIT = 1; public static final int SAVE_MODE_JOIN = 2; public static final int SAVE_MODE_DELETE = 3;//add by chenqiang private long mRawContactIdRequestingPhoto = -1; private static final int DIALOG_CONFIRM_DELETE = 1; private static final int DIALOG_CONFIRM_READONLY_DELETE = 2; private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3; private static final int DIALOG_CONFIRM_READONLY_HIDE = 4; String mQuerySelection; private long mContactIdForJoin; EntitySet mState; /** The linear layout holding the ContactEditorViews */ LinearLayout mContent; private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList(); @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); final Intent intent = getIntent(); final String action = intent.getAction(); setContentView(R.layout.act_edit); // Build editor and listen for photo requests mContent = (LinearLayout) findViewById(R.id.editors); findViewById(R.id.btn_done).setOnClickListener(this); findViewById(R.id.btn_discard).setOnClickListener(this); // Handle initial actions only when existing state missing final boolean hasIncomingState = icicle != null && icicle.containsKey(KEY_EDIT_STATE); if (TIntent.ACTION_EDIT.equals(action) && !hasIncomingState) { // Read initial state from database new QueryEntitiesTask(this).execute(intent); } else if (TIntent.ACTION_INSERT.equals(action) && !hasIncomingState) { // Trigger dialog to pick account type doAddAction(); } } private static class QueryEntitiesTask extends WeakAsyncTask<Intent, Void, Void, EditContactActivity> { public QueryEntitiesTask(EditContactActivity target) { super(target); } @Override protected Void doInBackground(EditContactActivity target, Intent... params) { // Load edit details in background final Context context = target; final Sources sources = Sources.getInstance(context); final Intent intent = params[0]; final ContentResolver resolver = context.getContentResolver(); // Handle both legacy and new authorities final Uri data = intent.getData(); final String authority = data.getAuthority(); final String mimeType = intent.resolveType(resolver); String selection = "0"; if (TContactsContract.AUTHORITY.equals(authority)) { if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) { // Handle selected aggregate final long contactId = ContentUris.parseId(data); selection = RawContacts.CONTACT_ID + "=" + contactId; } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) { final long rawContactId = ContentUris.parseId(data); final long contactId = ContactsUtils.queryForContactId( resolver, rawContactId); selection = RawContacts.CONTACT_ID + "=" + contactId; } } else if (android.provider.Contacts.AUTHORITY.equals(authority)) { final long rawContactId = ContentUris.parseId(data); selection = Data.RAW_CONTACT_ID + "=" + rawContactId; } target.mQuerySelection = selection; target.mState = EntitySet .fromQuery(resolver, selection, null, null); // Handle any incoming values that should be inserted final Bundle extras = intent.getExtras(); final boolean hasExtras = extras != null && extras.size() > 0; final boolean hasState = target.mState.size() > 0; if (hasExtras && hasState) { // Find source defining the first RawContact found final EntityDelta state = target.mState.get(0); final String accountType = state.getValues().getAsString( RawContacts.ACCOUNT_TYPE); final ContactsSource source = sources.getInflatedSource( accountType, ContactsSource.LEVEL_CONSTRAINTS); EntityModifier.parseExtras(context, source, state, extras); } return null; } @Override protected void onPostExecute(EditContactActivity target, Void result) { // Bind UI to new background state target.bindEditors(); } } @Override protected void onSaveInstanceState(Bundle outState) { if (hasValidState()) { // Store entities with modifications outState.putParcelable(KEY_EDIT_STATE, mState); } outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); super.onSaveInstanceState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { // Read modifications from instance mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE); mRawContactIdRequestingPhoto = savedInstanceState .getLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); bindEditors(); super.onRestoreInstanceState(savedInstanceState); } @Override protected void onDestroy() { super.onDestroy(); for (Dialog dialog : mManagedDialogs) { if (dialog.isShowing()) { dialog.dismiss(); } } } @Override protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_CONFIRM_DELETE: return new AlertDialog.Builder(this) .setTitle(R.string.deleteConfirmation_title) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(R.string.deleteConfirmation) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new DeleteClickListener()).setCancelable(false) .create(); case DIALOG_CONFIRM_READONLY_DELETE: return new AlertDialog.Builder(this) .setTitle(R.string.deleteConfirmation_title) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(R.string.readOnlyContactDeleteConfirmation) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new DeleteClickListener()).setCancelable(false) .create(); case DIALOG_CONFIRM_MULTIPLE_DELETE: return new AlertDialog.Builder(this) .setTitle(R.string.deleteConfirmation_title) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(R.string.multipleContactDeleteConfirmation) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new DeleteClickListener()).setCancelable(false) .create(); case DIALOG_CONFIRM_READONLY_HIDE: return new AlertDialog.Builder(this) .setTitle(R.string.deleteConfirmation_title) .setIcon(android.R.drawable.ic_dialog_alert) .setMessage(R.string.readOnlyContactWarning) .setPositiveButton(android.R.string.ok, new DeleteClickListener()).setCancelable(false) .create(); } return null; } /** * Start managing this {@link Dialog} along with the {@link Activity}. */ private void startManagingDialog(Dialog dialog) { synchronized (mManagedDialogs) { mManagedDialogs.add(dialog); } } /** * Show this {@link Dialog} and manage with the {@link Activity}. */ void showAndManageDialog(Dialog dialog) { startManagingDialog(dialog); dialog.show(); } /** * Check if our internal {@link #mState} is valid, usually checked before * performing user actions. */ protected boolean hasValidState() { return mState != null && mState.size() > 0; } /** * Rebuild the editors to match our underlying {@link #mState} object, * usually called once we've parsed {@link Entity} data or have inserted a * new {@link RawContacts}. */ protected void bindEditors() { if (!hasValidState()) return; final LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); final Sources sources = Sources.getInstance(this); // Sort the editors Collections.sort(mState, this); // Remove any existing editors and rebuild any visible mContent.removeAllViews(); int size = mState.size(); for (int i = 0; i < size; i++) { // TODO ensure proper ordering of entities in the list EntityDelta entity = mState.get(i); final ValuesDelta values = entity.getValues(); if (!values.isVisible()) continue; final String accountType = values .getAsString(RawContacts.ACCOUNT_TYPE); final ContactsSource source = sources.getInflatedSource( accountType, ContactsSource.LEVEL_CONSTRAINTS); final long rawContactId = values.getAsLong(RawContacts._ID); BaseContactEditorView editor; if (!source.readOnly) { editor = (BaseContactEditorView) inflater.inflate( R.layout.item_contact_editor, mContent, false); } else { editor = (BaseContactEditorView) inflater .inflate(R.layout.item_read_only_contact_editor, mContent, false); } PhotoEditorView photoEditor = editor.getPhotoEditor(); photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly, photoEditor)); mContent.addView(editor); editor.setState(entity, source); } // Show editor now that we've loaded state mContent.setVisibility(View.VISIBLE); } /** * Class that listens to requests coming from photo editors */ private class PhotoListener implements EditorListener, DialogInterface.OnClickListener { private long mRawContactId; private boolean mReadOnly; private PhotoEditorView mEditor; public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) { mRawContactId = rawContactId; mReadOnly = readOnly; mEditor = editor; } public void onDeleted(Editor editor) { // Do nothing } public void onRequest(int request) { if (!hasValidState()) return; if (request == EditorListener.REQUEST_PICK_PHOTO) { if (mEditor.hasSetPhoto()) { // There is an existing photo, offer to remove, replace, or // promoto to primary createPhotoDialog().show(); } else if (!mReadOnly) { // No photo set and not read-only, try to set the photo doPickPhotoAction(mRawContactId); } } } /** * Prepare dialog for picking a new {@link EditType} or entering a * custom label. This dialog is limited to the valid types as determined * by {@link EntityModifier}. */ public Dialog createPhotoDialog() { Context context = EditContactActivity.this; // Wrap our context to inflate list items using correct theme final Context dialogContext = new ContextThemeWrapper(context, android.R.style.Theme_Light); String[] choices; if (mReadOnly) { choices = new String[1]; choices[0] = getString(R.string.use_photo_as_primary); } else { choices = new String[3]; choices[0] = getString(R.string.use_photo_as_primary); choices[1] = getString(R.string.removePicture); choices[2] = getString(R.string.changePicture); } final ListAdapter adapter = new ArrayAdapter<String>(dialogContext, android.R.layout.simple_list_item_1, choices); final AlertDialog.Builder builder = new AlertDialog.Builder( dialogContext); builder.setTitle(R.string.attachToContact); builder.setSingleChoiceItems(adapter, -1, this); return builder.create(); } /** * Called when something in the dialog is clicked */ public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); switch (which) { case 0: // Set the photo as super primary mEditor.setSuperPrimary(true); // And set all other photos as not super primary int count = mContent.getChildCount(); for (int i = 0; i < count; i++) { View childView = mContent.getChildAt(i); if (childView instanceof BaseContactEditorView) { BaseContactEditorView editor = (BaseContactEditorView) childView; PhotoEditorView photoEditor = editor.getPhotoEditor(); if (!photoEditor.equals(mEditor)) { photoEditor.setSuperPrimary(false); } } } break; case 1: // Remove the photo mEditor.setPhotoBitmap(null); break; case 2: // Pick a new photo for the contact doPickPhotoAction(mRawContactId); break; } } } /** {@inheritDoc} */ public void onClick(View view) { switch (view.getId()) { case R.id.btn_done: doSaveAction(SAVE_MODE_DEFAULT); break; case R.id.btn_discard: doRevertAction(); break; } } /** {@inheritDoc} */ @Override public void onBackPressed() { doSaveAction(SAVE_MODE_DEFAULT); } /** {@inheritDoc} */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Ignore failed requests if (resultCode != RESULT_OK) return; switch (requestCode) { case PHOTO_PICKED_WITH_DATA: { BaseContactEditorView requestingEditor = null; for (int i = 0; i < mContent.getChildCount(); i++) { View childView = mContent.getChildAt(i); if (childView instanceof BaseContactEditorView) { BaseContactEditorView editor = (BaseContactEditorView) childView; if (editor.getRawContactId() == mRawContactIdRequestingPhoto) { requestingEditor = editor; break; } } } if (requestingEditor != null) { final Bitmap photo = data.getParcelableExtra("data"); requestingEditor.setPhotoBitmap(photo); mRawContactIdRequestingPhoto = -1; } else { // The contact that requested the photo is no longer present. // TODO: Show error message } break; } case REQUEST_JOIN_CONTACT: { if (resultCode == RESULT_OK && data != null) { final long contactId = ContentUris.parseId(data.getData()); joinAggregate(contactId); } } } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.edit, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { //add by chenqiang ע�͵���ֺϲ� // menu.findItem(R.id.menu_split).setVisible( // mState != null && mState.size() > 1); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_done: return doSaveAction(SAVE_MODE_DEFAULT); case R.id.menu_discard: return doRevertAction(); case R.id.menu_add: return doAddAction(); //�����ɾ�����������ϵͳ�쳣�������ε� �� modified by Boern /**/ case R.id.menu_delete: return doDeleteAction(); // case R.id.menu_split: // return doSplitContactAction(); // case R.id.menu_join: // return doJoinContactAction(); } return false; } /** * Background task for persisting edited contact data, using the changes * defined by a set of {@link EntityDelta}. This task starts * {@link EmptyService} to make sure the background thread can finish * persisting in cases where the system wants to reclaim our process. */ public static class PersistTask extends WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> { private static final int PERSIST_TRIES = 3; private static final int RESULT_UNCHANGED = 0; private static final int RESULT_SUCCESS = 1; private static final int RESULT_FAILURE = 2; private WeakReference<ProgressDialog> mProgress; private int mSaveMode; private Uri mContactLookupUri = null; public PersistTask(EditContactActivity target, int saveMode) { super(target); mSaveMode = saveMode; } /** {@inheritDoc} */ @Override protected void onPreExecute(EditContactActivity target) { mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show( target, null, target.getText(R.string.savingContact))); // Before starting this task, start an empty service to protect our // process from being reclaimed by the system. final Context context = target; context.startService(new Intent(context, EmptyService.class)); } /** {@inheritDoc} */ @Override protected Integer doInBackground(EditContactActivity target, EntitySet... params) { final Context context = target; final ContentResolver resolver = context.getContentResolver(); EntitySet state = params[0]; // Trim any empty fields, and RawContacts, before persisting final Sources sources = Sources.getInstance(context); EntityModifier.trimEmpty(state, sources); // Attempt to persist changes int tries = 0; Integer result = RESULT_FAILURE; while (tries++ < PERSIST_TRIES) { try { // Build operations and try applying final ArrayList<ContentProviderOperation> diff = state .buildDiff(); ContentProviderResult[] results = null; if (!diff.isEmpty()) { results = resolver.applyBatch( TContactsContract.AUTHORITY, diff); } final long rawContactId = getRawContactId(state, diff, results); if (rawContactId != -1) { final Uri rawContactUri = ContentUris.withAppendedId( TRawContacts.CONTENT_URI, rawContactId); // convert the raw contact URI to a contact URI mContactLookupUri = TRawContacts.getContactLookupUri( resolver, rawContactUri); } result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED; break; } catch (RemoteException e) { // Something went wrong, bail without success Log.e(TAG, "Problem persisting user edits", e); break; } catch (OperationApplicationException e) { // Version consistency failed, re-parent change and try // again Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString()); final EntitySet newState = EntitySet.fromQuery(resolver, target.mQuerySelection, null, null); state = EntitySet.mergeAfter(newState, state); } } return result; } public final static int TYPE_INSERT = 1; private long getRawContactId(EntitySet state, final ArrayList<ContentProviderOperation> diff, final ContentProviderResult[] results) { long rawContactId = state.findRawContactId(); if (rawContactId != -1) { return rawContactId; } // we gotta do some searching for the id final int diffSize = diff.size(); for (int i = 0; i < diffSize; i++) { ContentProviderOperation operation = diff.get(i); try { Method operationMethod = Class .forName( "android.content.ContentProviderOperation") .getMethod("getType", new Class[] {}); operationMethod.setAccessible(true); int type = (Integer) operationMethod.invoke(operation, new Object[] {}); if (type == TContentProviderOperation.TYPE_INSERT && operation .getUri() .getEncodedPath() .contains( TRawContacts.CONTENT_URI .getEncodedPath())) { return ContentUris.parseId(results[i].uri); } } catch (Exception e) { Log.d("TContact", e.getMessage()); } } return -1; } /** {@inheritDoc} */ @Override protected void onPostExecute(EditContactActivity target, Integer result) { final Context context = target; final ProgressDialog progress = mProgress.get(); if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) { //ע�͵� by Boern // Toast.makeText(context, R.string.contactSavedToast, // Toast.LENGTH_SHORT).show(); } else if (result == RESULT_FAILURE) { Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); } // Only dismiss when valid reference and still showing if (progress != null && progress.isShowing()) { progress.dismiss(); } // Stop the service that was protecting us context.stopService(new Intent(context, EmptyService.class)); //add by chenqiang if(mContactLookupUri!=null && mSaveMode==SAVE_MODE_DEFAULT){//�޸���ϵ�� ContactsUtility.markContact(context, mContactLookupUri, SyncState.SYNC_STATE_UPDATED); }else if(mContactLookupUri!=null && mSaveMode==SAVE_MODE_DELETE){//ɾ����ϵ�� ContactsUtility.markContact(context, mContactLookupUri, SyncState.SYNC_STATE_DELETE); ContentValues values = new ContentValues(1); values.put(Contacts.STARRED,0); context.getContentResolver().update(mContactLookupUri, values, null, null); } target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri); } } /** * Saves or creates the contact based on the mode, and if successful * finishes the activity. */ boolean doSaveAction(int saveMode) { if (!hasValidState()) return false; final PersistTask task = new PersistTask(this, saveMode); task.execute(mState); return true; } private class DeleteClickListener implements DialogInterface.OnClickListener { public void onClick(DialogInterface dialog, int which) { Sources sources = Sources.getInstance(EditContactActivity.this); // Mark all raw contacts for deletion for (EntityDelta delta : mState) { // delta.markDeleted();//modify by chenqiang } // Save the deletes // doSaveAction(SAVE_MODE_DEFAULT); doSaveAction(SAVE_MODE_DELETE);//add by chenqiang // finish();//modify by chenqiang �˴����������쳣 } } private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) { switch (saveMode) { case SAVE_MODE_DEFAULT: if (success && contactLookupUri != null) { final Intent resultIntent = new Intent(); final Uri requestData = getIntent().getData(); final String requestAuthority = requestData == null ? null : requestData.getAuthority(); if (android.provider.Contacts.AUTHORITY .equals(requestAuthority)) { // Build legacy Uri when requested by caller final long contactId = ContentUris.parseId(TContacts .lookupContact(getContentResolver(), contactLookupUri)); final Uri legacyUri = ContentUris.withAppendedId( android.provider.Contacts.People.CONTENT_URI, contactId); resultIntent.setData(legacyUri); } else { // Otherwise pass back a lookup-style Uri resultIntent.setData(contactLookupUri); } setResult(RESULT_OK, resultIntent); } else { setResult(RESULT_CANCELED, null); } finish(); break; case SAVE_MODE_SPLIT: if (success) { Intent intent = new Intent(); intent.setData(contactLookupUri); setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent); } finish(); break; case SAVE_MODE_JOIN: if (success) { showJoinAggregateActivity(contactLookupUri); } break; //add by chenqiang case SAVE_MODE_DELETE: if (success && contactLookupUri != null) { final Intent resultIntent = new Intent(); final Uri requestData = getIntent().getData(); final String requestAuthority = requestData == null ? null : requestData.getAuthority(); if (android.provider.Contacts.AUTHORITY .equals(requestAuthority)) { // Build legacy Uri when requested by caller final long contactId = ContentUris.parseId(TContacts .lookupContact(getContentResolver(), contactLookupUri)); final Uri legacyUri = ContentUris.withAppendedId( android.provider.Contacts.People.CONTENT_URI, contactId); resultIntent.setData(legacyUri); } else { // Otherwise pass back a lookup-style Uri resultIntent.setData(contactLookupUri); } setResult(RESULT_OK, resultIntent); } else { setResult(RESULT_CANCELED, null); } finish(); 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) */ public void showJoinAggregateActivity(Uri contactLookupUri) { if (contactLookupUri == null) { return; } mContactIdForJoin = ContentUris.parseId(contactLookupUri); Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE); intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin); startActivityForResult(intent, REQUEST_JOIN_CONTACT); } /** * Performs aggregation with the contact selected by the user from * suggestions or A-Z list. */ private void joinAggregate(final long contactId) { ContentResolver resolver = getContentResolver(); // Load raw contact IDs for all raw contacts involved - currently edited // and selected // in the join UIs Cursor c = resolver.query(TRawContacts.CONTENT_URI, new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=" + contactId + " OR " + RawContacts.CONTACT_ID + "=" + mContactIdForJoin, null, null); long rawContactIds[]; try { rawContactIds = new long[c.getCount()]; for (int i = 0; i < rawContactIds.length; i++) { c.moveToNext(); rawContactIds[i] = c.getLong(0); } } finally { c.close(); } // For each pair of raw contacts, insert an aggregation exception ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); for (int i = 0; i < rawContactIds.length; i++) { for (int j = 0; j < rawContactIds.length; j++) { if (i != j) { buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]); } } } // Apply all aggregation exceptions as one batch try { getContentResolver().applyBatch(TContactsContract.AUTHORITY, operations); // We can use any of the constituent raw contacts to refresh the UI // - why not the first Intent intent = new Intent(); intent.setData(ContentUris.withAppendedId(TRawContacts.CONTENT_URI, rawContactIds[0])); // Reload the new state from database new QueryEntitiesTask(this).execute(intent); Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show(); } catch (RemoteException e) { Log.e(TAG, "Failed to apply aggregation exception batch", e); Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); } catch (OperationApplicationException e) { Log.e(TAG, "Failed to apply aggregation exception batch", e); Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); } } /** * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} * ContentProviderOperation. */ private void buildJoinContactDiff( ArrayList<ContentProviderOperation> operations, long rawContactId1, long rawContactId2) { // Builder builder = ContentProviderOperation // .newUpdate(AggregationExceptions.CONTENT_URI); //add by chenqiang Builder builder = ContentProviderOperation .newUpdate(TAggregationExceptions.CONTENT_URI); builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); operations.add(builder.build()); } /** * Revert any changes the user has made, and finish the activity. */ private boolean doRevertAction() { finish(); return true; } /** * Create a new {@link RawContacts} which will exist as another * {@link EntityDelta} under the currently edited {@link Contacts}. */ private boolean doAddAction() { // Adding is okay when missing state new AddContactTask(this).execute(); return true; } /** * Delete the entire contact currently being edited, which usually asks for * user confirmation before continuing. */ private boolean doDeleteAction() { if (!hasValidState()) return false; int readOnlySourcesCnt = 0; int writableSourcesCnt = 0; Sources sources = Sources.getInstance(EditContactActivity.this); for (EntityDelta delta : mState) { final String accountType = delta.getValues().getAsString( RawContacts.ACCOUNT_TYPE); final ContactsSource contactsSource = sources.getInflatedSource( accountType, ContactsSource.LEVEL_CONSTRAINTS); if (contactsSource != null && contactsSource.readOnly) { readOnlySourcesCnt += 1; } else { writableSourcesCnt += 1; } } if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) { showDialog(DIALOG_CONFIRM_READONLY_DELETE); } else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) { showDialog(DIALOG_CONFIRM_READONLY_HIDE); } else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) { showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE); } else { showDialog(DIALOG_CONFIRM_DELETE); } return true; } /** * Pick a specific photo to be added under the currently selected tab. */ boolean doPickPhotoAction(long rawContactId) { if (!hasValidState()) return false; try { // Launch picker to choose photo for selected contact final Intent intent = ContactsUtils.getPhotoPickIntent(); startActivityForResult(intent, PHOTO_PICKED_WITH_DATA); mRawContactIdRequestingPhoto = rawContactId; } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); } return true; } /** {@inheritDoc} */ public void onDeleted(Editor editor) { // Ignore any editor deletes } private boolean doSplitContactAction() { if (!hasValidState()) return false; showAndManageDialog(createSplitDialog()); return true; } private Dialog createSplitDialog() { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.splitConfirmation_title); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(R.string.splitConfirmation); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Split the contacts mState.splitRawContacts(); doSaveAction(SAVE_MODE_SPLIT); } }); builder.setNegativeButton(android.R.string.cancel, null); builder.setCancelable(false); return builder.create(); } private boolean doJoinContactAction() { return doSaveAction(SAVE_MODE_JOIN); } /** * Build dialog that handles adding a new {@link RawContacts} after the user * picks a specific {@link ContactsSource}. */ private static class AddContactTask extends WeakAsyncTask<Void, Void, AlertDialog.Builder, EditContactActivity> { public AddContactTask(EditContactActivity target) { super(target); } @Override protected AlertDialog.Builder doInBackground( final EditContactActivity target, Void... params) { final Sources sources = Sources.getInstance(target); // Wrap our context to inflate list items using correct theme final Context dialogContext = new ContextThemeWrapper(target, android.R.style.Theme_Light); final LayoutInflater dialogInflater = (LayoutInflater) dialogContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); final ArrayList<Account> writable = sources.getAccounts(true); // No Accounts available. Create a phone-local contact. if (writable.isEmpty()) { selectAccount(null); return null; // Don't show a dialog. } // In the common case of a single account being writable, // auto-select // it without showing a dialog. if (writable.size() == 1) { selectAccount(writable.get(0)); return null; // Don't show a dialog. } final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>( target, android.R.layout.simple_list_item_2, writable) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = dialogInflater.inflate( android.R.layout.simple_list_item_2, parent, false); } // TODO: show icon along with title final TextView text1 = (TextView) convertView .findViewById(android.R.id.text1); final TextView text2 = (TextView) convertView .findViewById(android.R.id.text2); final Account account = this.getItem(position); final ContactsSource source = sources.getInflatedSource( account.type, ContactsSource.LEVEL_SUMMARY); text1.setText(account.name); text2.setText(source.getDisplayLabel(target)); return convertView; } }; final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); // Create new contact based on selected source final Account account = accountAdapter.getItem(which); selectAccount(account); // Update the UI. EditContactActivity target = mTarget.get(); if (target != null) { target.bindEditors(); } } }; final DialogInterface.OnCancelListener cancelListener = new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { // If nothing remains, close activity if (!target.hasValidState()) { target.finish(); } } }; // TODO: when canceled and was single add, finish() final AlertDialog.Builder builder = new AlertDialog.Builder(target); builder.setTitle(R.string.dialog_new_contact_account); builder.setSingleChoiceItems(accountAdapter, 0, clickListener); builder.setOnCancelListener(cancelListener); return builder; } /** * Sets up EditContactActivity's mState for the account selected. Runs * from a background thread. * * @param account * may be null to signal a device-local contact should be * created. */ private void selectAccount(Account account) { EditContactActivity target = mTarget.get(); if (target == null) { return; } final Sources sources = Sources.getInstance(target); final ContentValues values = new ContentValues(); if (account != null) { values.put(RawContacts.ACCOUNT_NAME, account.name); values.put(RawContacts.ACCOUNT_TYPE, account.type); } else { values.putNull(RawContacts.ACCOUNT_NAME); values.putNull(RawContacts.ACCOUNT_TYPE); } // Parse any values from incoming intent final EntityDelta insert = new EntityDelta( ValuesDelta.fromAfter(values)); final ContactsSource source = sources.getInflatedSource( account != null ? account.type : null, ContactsSource.LEVEL_CONSTRAINTS); final Bundle extras = target.getIntent().getExtras(); EntityModifier.parseExtras(target, source, insert, extras); // Ensure we have some default fields EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE); EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE); // Create "My Contacts" membership for Google contacts // TODO: move this off into "templates" for each given source if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) { GoogleSource.attemptMyContactsMembership(insert, target); } // TODO: no synchronization here on target.mState. This // runs in the background thread, but it's accessed from // multiple thread, including the UI thread. if (target.mState == null) { // Create state if none exists yet target.mState = EntitySet.fromSingle(insert); } else { // Add contact onto end of existing state target.mState.add(insert); } } @Override protected void onPostExecute(EditContactActivity target, AlertDialog.Builder result) { if (result != null) { // Note: null is returned when no dialog is to be // shown (no multiple accounts to select between) target.showAndManageDialog(result.create()); } else { // Account was auto-selected on the background thread, // but we need to update the UI still in the // now-current UI thread. target.bindEditors(); } } } private Dialog createDeleteDialog() { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.deleteConfirmation_title); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(R.string.deleteConfirmation); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // Mark all raw contacts for deletion for (EntityDelta delta : mState) { delta.markDeleted(); } // Save the deletes doSaveAction(SAVE_MODE_DEFAULT); finish(); } }); builder.setNegativeButton(android.R.string.cancel, null); builder.setCancelable(false); return builder.create(); } /** * Create dialog for selecting primary display name. */ private Dialog createNameDialog() { // Build set of all available display names final ArrayList<ValuesDelta> allNames = Lists.newArrayList(); for (EntityDelta entity : mState) { final ArrayList<ValuesDelta> displayNames = entity .getMimeEntries(StructuredName.CONTENT_ITEM_TYPE); allNames.addAll(displayNames); } // Wrap our context to inflate list items using correct theme final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); final LayoutInflater dialogInflater = this.getLayoutInflater() .cloneInContext(dialogContext); final ListAdapter nameAdapter = new ArrayAdapter<ValuesDelta>(this, android.R.layout.simple_list_item_1, allNames) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = dialogInflater.inflate( android.R.layout.simple_list_item_1, parent, false); } final ValuesDelta structuredName = this.getItem(position); final String displayName = structuredName .getAsString(StructuredName.DISPLAY_NAME); ((TextView) convertView).setText(displayName); return convertView; } }; final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); // User picked display name, so make super-primary final ValuesDelta structuredName = allNames.get(which); structuredName.put(Data.IS_PRIMARY, 1); structuredName.put(Data.IS_SUPER_PRIMARY, 1); } }; final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_primary_name); builder.setSingleChoiceItems(nameAdapter, 0, clickListener); return builder.create(); } /** * Compare EntityDeltas for sorting the stack of editors. */ public int compare(EntityDelta one, EntityDelta two) { // Check direct equality if (one.equals(two)) { return 0; } final Sources sources = Sources.getInstance(this); String accountType = one.getValues().getAsString( RawContacts.ACCOUNT_TYPE); final ContactsSource oneSource = sources.getInflatedSource(accountType, ContactsSource.LEVEL_SUMMARY); accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); final ContactsSource twoSource = sources.getInflatedSource(accountType, ContactsSource.LEVEL_SUMMARY); // Check read-only if (oneSource.readOnly && !twoSource.readOnly) { return 1; } else if (twoSource.readOnly && !oneSource.readOnly) { return -1; } // Check account type boolean skipAccountTypeCheck = false; boolean oneIsGoogle = oneSource instanceof GoogleSource; boolean twoIsGoogle = twoSource instanceof GoogleSource; if (oneIsGoogle && !twoIsGoogle) { return -1; } else if (twoIsGoogle && !oneIsGoogle) { return 1; } else { skipAccountTypeCheck = true; } int value; if (!skipAccountTypeCheck) { value = oneSource.accountType.compareTo(twoSource.accountType); if (value != 0) { return value; } } // Check account name ValuesDelta oneValues = one.getValues(); String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME); if (oneAccount == null) oneAccount = ""; ValuesDelta twoValues = two.getValues(); String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME); if (twoAccount == null) twoAccount = ""; value = oneAccount.compareTo(twoAccount); if (value != 0) { return value; } // Both are in the same account, fall back to contact ID long oneId = oneValues.getAsLong(RawContacts._ID); long twoId = twoValues.getAsLong(RawContacts._ID); return (int) (oneId - twoId); } }