2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.contacts;
19 import static com.android.contacts.ContactEntryAdapter.CONTACT_CUSTOM_RINGTONE_COLUMN;
20 import static com.android.contacts.ContactEntryAdapter.CONTACT_NAME_COLUMN;
21 import static com.android.contacts.ContactEntryAdapter.CONTACT_NOTES_COLUMN;
22 import static com.android.contacts.ContactEntryAdapter.CONTACT_PROJECTION;
23 import static com.android.contacts.ContactEntryAdapter.CONTACT_SEND_TO_VOICEMAIL_COLUMN;
24 import static com.android.contacts.ContactEntryAdapter.CONTACT_PHONETIC_NAME_COLUMN;
25 import static com.android.contacts.ContactEntryAdapter.METHODS_AUX_DATA_COLUMN;
26 import static com.android.contacts.ContactEntryAdapter.METHODS_DATA_COLUMN;
27 import static com.android.contacts.ContactEntryAdapter.METHODS_ID_COLUMN;
28 import static com.android.contacts.ContactEntryAdapter.METHODS_ISPRIMARY_COLUMN;
29 import static com.android.contacts.ContactEntryAdapter.METHODS_KIND_COLUMN;
30 import static com.android.contacts.ContactEntryAdapter.METHODS_LABEL_COLUMN;
31 import static com.android.contacts.ContactEntryAdapter.METHODS_PROJECTION;
32 import static com.android.contacts.ContactEntryAdapter.METHODS_TYPE_COLUMN;
33 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_COMPANY_COLUMN;
34 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_ID_COLUMN;
35 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_ISPRIMARY_COLUMN;
36 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_LABEL_COLUMN;
37 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_PROJECTION;
38 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_TITLE_COLUMN;
39 import static com.android.contacts.ContactEntryAdapter.ORGANIZATIONS_TYPE_COLUMN;
40 import static com.android.contacts.ContactEntryAdapter.PHONES_ID_COLUMN;
41 import static com.android.contacts.ContactEntryAdapter.PHONES_ISPRIMARY_COLUMN;
42 import static com.android.contacts.ContactEntryAdapter.PHONES_LABEL_COLUMN;
43 import static com.android.contacts.ContactEntryAdapter.PHONES_NUMBER_COLUMN;
44 import static com.android.contacts.ContactEntryAdapter.PHONES_PROJECTION;
45 import static com.android.contacts.ContactEntryAdapter.PHONES_TYPE_COLUMN;
47 import android.app.Activity;
48 import android.app.AlertDialog;
49 import android.app.Dialog;
50 import android.content.ActivityNotFoundException;
51 import android.content.ContentResolver;
52 import android.content.ContentUris;
53 import android.content.ContentValues;
54 import android.content.Context;
55 import android.content.DialogInterface;
56 import android.content.Intent;
57 import android.content.SharedPreferences;
58 import android.content.res.ColorStateList;
59 import android.content.res.Resources;
60 import android.database.Cursor;
61 import android.graphics.Bitmap;
62 import android.media.Ringtone;
63 import android.media.RingtoneManager;
64 import android.net.Uri;
65 import android.os.Bundle;
66 import android.os.Parcel;
67 import android.os.Parcelable;
68 import android.preference.PreferenceManager;
69 import android.provider.Contacts;
70 import android.provider.Contacts.ContactMethods;
71 import android.provider.Contacts.Intents.Insert;
72 import android.provider.Contacts.Groups;
73 import android.provider.Contacts.Organizations;
74 import android.provider.Contacts.People;
75 import android.provider.Contacts.Phones;
76 import android.telephony.PhoneNumberFormattingTextWatcher;
77 import android.text.Editable;
78 import android.text.TextUtils;
79 import android.text.TextWatcher;
80 import android.text.method.TextKeyListener;
81 import android.text.method.TextKeyListener.Capitalize;
82 import android.util.Log;
83 import android.util.SparseBooleanArray;
84 import android.view.KeyEvent;
85 import android.view.LayoutInflater;
86 import android.view.Menu;
87 import android.view.MenuItem;
88 import android.view.View;
89 import android.view.ViewGroup;
90 import android.view.ViewParent;
91 import android.view.inputmethod.EditorInfo;
92 import android.widget.Button;
93 import android.widget.CheckBox;
94 import android.widget.EditText;
95 import android.widget.ImageView;
96 import android.widget.LinearLayout;
97 import android.widget.TextView;
98 import android.widget.Toast;
100 import java.io.ByteArrayOutputStream;
101 import java.util.ArrayList;
104 * Activity for editing or inserting a contact. Note that if the contact data changes in the
105 * background while this activity is running, the updates will be overwritten.
107 public final class EditContactActivity extends Activity implements View.OnClickListener,
108 TextWatcher, View.OnFocusChangeListener {
109 private static final String TAG = "EditContactActivity";
111 private static final int STATE_UNKNOWN = 0;
112 /** Editing an existing contact */
113 private static final int STATE_EDIT = 1;
114 /** The full insert mode */
115 private static final int STATE_INSERT = 2;
117 /** The launch code when picking a photo and the raw data is returned */
118 private static final int PHOTO_PICKED_WITH_DATA = 3021;
120 /** The launch code when picking a ringtone */
121 private static final int RINGTONE_PICKED = 3023;
123 // These correspond to the string array in resources for picker "other" items
124 final static int OTHER_ORGANIZATION = 0;
125 final static int OTHER_NOTE = 1;
128 final static int DELETE_CONFIRMATION_DIALOG = 2;
131 final static int SECTION_PHONES = 3;
132 final static int SECTION_EMAIL = 4;
133 final static int SECTION_IM = 5;
134 final static int SECTION_POSTAL = 6;
135 final static int SECTION_ORG = 7;
136 final static int SECTION_NOTE = 8;
139 public static final int MENU_ITEM_SAVE = 1;
140 public static final int MENU_ITEM_DONT_SAVE = 2;
141 public static final int MENU_ITEM_DELETE = 3;
142 public static final int MENU_ITEM_PHOTO = 6;
144 /** Used to represent an invalid type for a contact entry */
145 private static final int INVALID_TYPE = -1;
147 /** The default type for a phone that is added via an intent */
148 private static final int DEFAULT_PHONE_TYPE = Phones.TYPE_MOBILE;
150 /** The default type for an email that is added via an intent */
151 private static final int DEFAULT_EMAIL_TYPE = ContactMethods.TYPE_HOME;
153 /** The default type for a postal address that is added via an intent */
154 private static final int DEFAULT_POSTAL_TYPE = ContactMethods.TYPE_HOME;
156 private int mState; // saved across instances
157 private boolean mInsert; // saved across instances
158 private Uri mUri; // saved across instances
159 /** In insert mode this is the photo */
160 private Bitmap mPhoto; // saved across instances
161 private boolean mPhotoChanged = false; // saved across instances
163 private EditText mNameView;
164 private ImageView mPhotoImageView;
165 private ViewGroup mContentView;
166 private LinearLayout mLayout;
167 private LayoutInflater mInflater;
168 private MenuItem mPhotoMenuItem;
169 private boolean mPhotoPresent = false;
170 private EditText mPhoneticNameView; // invisible in some locales, but always present
172 /** Flag marking this contact as changed, meaning we should write changes back. */
173 private boolean mContactChanged = false;
175 // These are accessed by inner classes. They're package scoped to make access more efficient.
176 /* package */ ContentResolver mResolver;
177 /* package */ ArrayList<EditEntry> mPhoneEntries = new ArrayList<EditEntry>();
178 /* package */ ArrayList<EditEntry> mEmailEntries = new ArrayList<EditEntry>();
179 /* package */ ArrayList<EditEntry> mImEntries = new ArrayList<EditEntry>();
180 /* package */ ArrayList<EditEntry> mPostalEntries = new ArrayList<EditEntry>();
181 /* package */ ArrayList<EditEntry> mOrgEntries = new ArrayList<EditEntry>();
182 /* package */ ArrayList<EditEntry> mNoteEntries = new ArrayList<EditEntry>();
183 /* package */ ArrayList<EditEntry> mOtherEntries = new ArrayList<EditEntry>();
184 /* package */ ArrayList<ArrayList<EditEntry>> mSections = new ArrayList<ArrayList<EditEntry>>();
186 /* package */ static final int MSG_DELETE = 1;
187 /* package */ static final int MSG_CHANGE_LABEL = 2;
188 /* package */ static final int MSG_ADD_PHONE = 3;
189 /* package */ static final int MSG_ADD_EMAIL = 4;
190 /* package */ static final int MSG_ADD_POSTAL = 5;
192 private static final int[] TYPE_PRECEDENCE_PHONES = new int[] {
193 Phones.TYPE_MOBILE, Phones.TYPE_HOME, Phones.TYPE_WORK, Phones.TYPE_OTHER
195 private static final int[] TYPE_PRECEDENCE_METHODS = new int[] {
196 ContactMethods.TYPE_HOME, ContactMethods.TYPE_WORK, ContactMethods.TYPE_OTHER
198 private static final int[] TYPE_PRECEDENCE_IM = new int[] {
199 ContactMethods.PROTOCOL_GOOGLE_TALK, ContactMethods.PROTOCOL_AIM,
200 ContactMethods.PROTOCOL_MSN, ContactMethods.PROTOCOL_YAHOO,
201 ContactMethods.PROTOCOL_JABBER
203 private static final int[] TYPE_PRECEDENCE_ORG = new int[] {
204 Organizations.TYPE_WORK, Organizations.TYPE_OTHER
207 public void onClick(View v) {
209 case R.id.photoImage: {
214 case R.id.checkable: {
215 CheckBox checkBox = (CheckBox) v.findViewById(R.id.checkbox);
218 EditEntry entry = findEntryForView(v);
219 entry.data = checkBox.isChecked() ? "1" : "0";
221 mContactChanged = true;
225 case R.id.entry_ringtone: {
226 EditEntry entry = findEntryForView(v);
227 doPickRingtone(entry);
231 case R.id.separator: {
232 // Someone clicked on a section header, so handle add action
233 int sectionType = (Integer) v.getTag();
234 doAddAction(sectionType);
238 case R.id.saveButton:
242 case R.id.discardButton:
247 EditEntry entry = findEntryForView(v);
249 // Clear the text and hide the view so it gets saved properly
250 ((TextView) entry.view.findViewById(R.id.data)).setText(null);
251 entry.view.setVisibility(View.GONE);
252 entry.isDeleted = true;
255 // Force rebuild of views because section headers might need to change
261 EditEntry entry = findEntryForView(v);
263 String[] labels = getLabelsForKind(this, entry.kind);
264 LabelPickedListener listener = new LabelPickedListener(entry, labels);
265 new AlertDialog.Builder(EditContactActivity.this)
266 .setItems(labels, listener)
267 .setTitle(R.string.selectLabel)
275 private void setPhotoPresent(boolean present) {
276 mPhotoPresent = present;
278 // Correctly scale the contact photo if present, otherwise just center
279 // the photo placeholder icon.
281 mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
283 mPhotoImageView.setImageResource(R.drawable.ic_menu_add_picture);
284 mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER);
287 if (mPhotoMenuItem != null) {
289 mPhotoMenuItem.setTitle(R.string.removePicture);
290 mPhotoMenuItem.setIcon(android.R.drawable.ic_menu_delete);
292 mPhotoMenuItem.setTitle(R.string.addPicture);
293 mPhotoMenuItem.setIcon(R.drawable.ic_menu_add_picture);
298 private EditEntry findEntryForView(View v) {
299 // Try to find the entry for this view
300 EditEntry entry = null;
302 Object tag = v.getTag();
303 if (tag != null && tag instanceof EditEntry) {
304 entry = (EditEntry) tag;
307 ViewParent parent = v.getParent();
308 if (parent != null && parent instanceof View) {
318 private DialogInterface.OnClickListener mDeleteContactDialogListener =
319 new DialogInterface.OnClickListener() {
320 public void onClick(DialogInterface dialog, int button) {
321 mResolver.delete(mUri, null, null);
326 private boolean mMobilePhoneAdded = false;
327 private boolean mPrimaryEmailAdded = false;
330 protected void onCreate(Bundle icicle) {
331 super.onCreate(icicle);
333 mResolver = getContentResolver();
335 // Build the list of sections
339 mInflater = getLayoutInflater();
340 mContentView = (ViewGroup)mInflater.inflate(R.layout.edit_contact, null);
341 setContentView(mContentView);
343 mLayout = (LinearLayout) findViewById(R.id.list);
344 mNameView = (EditText) findViewById(R.id.name);
345 mPhotoImageView = (ImageView) findViewById(R.id.photoImage);
346 mPhotoImageView.setOnClickListener(this);
347 mPhoneticNameView = (EditText) findViewById(R.id.phonetic_name);
349 // Setup the bottom buttons
350 View view = findViewById(R.id.saveButton);
351 view.setOnClickListener(this);
352 view = findViewById(R.id.discardButton);
353 view.setOnClickListener(this);
355 // Resolve the intent
356 mState = STATE_UNKNOWN;
357 Intent intent = getIntent();
358 String action = intent.getAction();
359 mUri = intent.getData();
361 if (action.equals(Intent.ACTION_EDIT)) {
362 if (icicle == null) {
363 // Build the entries & views
364 buildEntriesForEdit(getIntent().getExtras());
367 setTitle(R.string.editContact_title_edit);
369 } else if (action.equals(Intent.ACTION_INSERT)) {
370 if (icicle == null) {
371 // Build the entries & views
372 buildEntriesForInsert(getIntent().getExtras());
375 setTitle(R.string.editContact_title_insert);
376 mState = STATE_INSERT;
381 if (mState == STATE_UNKNOWN) {
382 Log.e(TAG, "Cannot resolve intent: " + intent);
387 if (mState == STATE_EDIT) {
388 setTitle(getResources().getText(R.string.editContact_title_edit));
390 setTitle(getResources().getText(R.string.editContact_title_insert));
394 private void setupSections() {
395 mSections.add(mPhoneEntries);
396 mSections.add(mEmailEntries);
397 mSections.add(mImEntries);
398 mSections.add(mPostalEntries);
399 mSections.add(mOrgEntries);
400 mSections.add(mNoteEntries);
401 mSections.add(mOtherEntries);
405 protected void onSaveInstanceState(Bundle outState) {
407 // To store current focus between config changes, follow focus down the
408 // view tree, keeping track of any parents with EditEntry tags
409 View focusedChild = mContentView.getFocusedChild();
410 EditEntry focusedEntry = null;
411 while (focusedChild != null) {
412 Object tag = focusedChild.getTag();
413 if (tag instanceof EditEntry) {
414 focusedEntry = (EditEntry) tag;
417 // Keep going deeper until child isn't a group
418 if (focusedChild instanceof ViewGroup) {
419 View deeperFocus = ((ViewGroup) focusedChild).getFocusedChild();
420 if (deeperFocus != null) {
421 focusedChild = deeperFocus;
430 if (focusedChild != null) {
431 int requestFocusId = focusedChild.getId();
432 int requestCursor = 0;
433 if (focusedChild instanceof EditText) {
434 requestCursor = ((EditText) focusedChild).getSelectionStart();
437 // Store focus values in EditEntry if found, otherwise store as
439 if (focusedEntry != null) {
440 focusedEntry.requestFocusId = requestFocusId;
441 focusedEntry.requestCursor = requestCursor;
443 outState.putInt("requestFocusId", requestFocusId);
444 outState.putInt("requestCursor", requestCursor);
448 outState.putParcelableArrayList("phoneEntries", mPhoneEntries);
449 outState.putParcelableArrayList("emailEntries", mEmailEntries);
450 outState.putParcelableArrayList("imEntries", mImEntries);
451 outState.putParcelableArrayList("postalEntries", mPostalEntries);
452 outState.putParcelableArrayList("orgEntries", mOrgEntries);
453 outState.putParcelableArrayList("noteEntries", mNoteEntries);
454 outState.putParcelableArrayList("otherEntries", mOtherEntries);
455 outState.putInt("state", mState);
456 outState.putBoolean("insert", mInsert);
457 outState.putParcelable("uri", mUri);
458 outState.putString("name", mNameView.getText().toString());
459 outState.putParcelable("photo", mPhoto);
460 outState.putBoolean("photoChanged", mPhotoChanged);
461 outState.putString("phoneticName", mPhoneticNameView.getText().toString());
462 outState.putBoolean("contactChanged", mContactChanged);
466 protected void onRestoreInstanceState(Bundle inState) {
467 mPhoneEntries = inState.getParcelableArrayList("phoneEntries");
468 mEmailEntries = inState.getParcelableArrayList("emailEntries");
469 mImEntries = inState.getParcelableArrayList("imEntries");
470 mPostalEntries = inState.getParcelableArrayList("postalEntries");
471 mOrgEntries = inState.getParcelableArrayList("orgEntries");
472 mNoteEntries = inState.getParcelableArrayList("noteEntries");
473 mOtherEntries = inState.getParcelableArrayList("otherEntries");
476 mState = inState.getInt("state");
477 mInsert = inState.getBoolean("insert");
478 mUri = inState.getParcelable("uri");
479 mNameView.setText(inState.getString("name"));
480 mPhoto = inState.getParcelable("photo");
481 if (mPhoto != null) {
482 mPhotoImageView.setImageBitmap(mPhoto);
483 setPhotoPresent(true);
485 mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
486 setPhotoPresent(false);
488 mPhotoChanged = inState.getBoolean("photoChanged");
489 mPhoneticNameView.setText(inState.getString("phoneticName"));
490 mContactChanged = inState.getBoolean("contactChanged");
492 // Now that everything is restored, build the view
495 // Try restoring any generally requested focus
496 int requestFocusId = inState.getInt("requestFocusId", View.NO_ID);
497 View focusedChild = mContentView.findViewById(requestFocusId);
498 if (focusedChild != null) {
499 focusedChild.requestFocus();
500 if (focusedChild instanceof EditText) {
501 int requestCursor = inState.getInt("requestCursor", 0);
502 ((EditText) focusedChild).setSelection(requestCursor);
508 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
509 if (resultCode != RESULT_OK) {
513 switch (requestCode) {
514 case PHOTO_PICKED_WITH_DATA: {
515 final Bundle extras = data.getExtras();
516 if (extras != null) {
517 Bitmap photo = extras.getParcelable("data");
519 mPhotoChanged = true;
520 mPhotoImageView.setImageBitmap(photo);
521 setPhotoPresent(true);
526 case RINGTONE_PICKED: {
527 Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
528 handleRingtonePicked(pickedUri);
529 mContactChanged = true;
536 public boolean onKeyDown(int keyCode, KeyEvent event) {
538 case KeyEvent.KEYCODE_BACK: {
543 return super.onKeyDown(keyCode, event);
547 public boolean onCreateOptionsMenu(Menu menu) {
548 super.onCreateOptionsMenu(menu);
549 menu.add(0, MENU_ITEM_SAVE, 0, R.string.menu_done)
550 .setIcon(android.R.drawable.ic_menu_save)
551 .setAlphabeticShortcut('\n');
552 menu.add(0, MENU_ITEM_DONT_SAVE, 0, R.string.menu_doNotSave)
553 .setIcon(android.R.drawable.ic_menu_close_clear_cancel)
554 .setAlphabeticShortcut('q');
556 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
557 .setIcon(android.R.drawable.ic_menu_delete);
560 mPhotoMenuItem = menu.add(0, MENU_ITEM_PHOTO, 0, null);
561 // Updates the state of the menu item
562 setPhotoPresent(mPhotoPresent);
568 public boolean onOptionsItemSelected(MenuItem item) {
569 switch (item.getItemId()) {
574 case MENU_ITEM_DONT_SAVE:
578 case MENU_ITEM_DELETE:
580 showDialog(DELETE_CONFIRMATION_DIALOG);
583 case MENU_ITEM_PHOTO:
584 if (!mPhotoPresent) {
587 doRemovePhotoAction();
596 * Try guessing the next-best type of {@link EditEntry} to insert into the
597 * given list. We walk down the precedence list until we find a type that
598 * doesn't exist yet, or default to the lowest ranking type.
600 private int guessNextType(ArrayList<EditEntry> entries, int[] precedenceList) {
601 // Keep track of the types we've seen already
602 SparseBooleanArray existAlready = new SparseBooleanArray(entries.size());
603 for (int i = entries.size() - 1; i >= 0; i--) {
604 EditEntry entry = entries.get(i);
605 if (!entry.isDeleted) {
606 existAlready.put(entry.type, true);
610 // Pick the first item we haven't seen
611 for (int type : precedenceList) {
612 if (!existAlready.get(type, false)) {
617 // Otherwise default to last item
618 return precedenceList[precedenceList.length - 1];
621 private void doAddAction(int sectionType) {
622 EditEntry entry = null;
623 switch (sectionType) {
624 case SECTION_PHONES: {
625 // Try figuring out which type to insert next
626 int nextType = guessNextType(mPhoneEntries, TYPE_PRECEDENCE_PHONES);
627 entry = EditEntry.newPhoneEntry(EditContactActivity.this,
628 Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY),
630 mPhoneEntries.add(entry);
633 case SECTION_EMAIL: {
634 // Try figuring out which type to insert next
635 int nextType = guessNextType(mEmailEntries, TYPE_PRECEDENCE_METHODS);
636 entry = EditEntry.newEmailEntry(EditContactActivity.this,
637 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
639 mEmailEntries.add(entry);
643 // Try figuring out which type to insert next
644 int nextType = guessNextType(mImEntries, TYPE_PRECEDENCE_IM);
645 entry = EditEntry.newImEntry(EditContactActivity.this,
646 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
648 mImEntries.add(entry);
651 case SECTION_POSTAL: {
652 int nextType = guessNextType(mPostalEntries, TYPE_PRECEDENCE_METHODS);
653 entry = EditEntry.newPostalEntry(EditContactActivity.this,
654 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
656 mPostalEntries.add(entry);
660 int nextType = guessNextType(mOrgEntries, TYPE_PRECEDENCE_ORG);
661 entry = EditEntry.newOrganizationEntry(EditContactActivity.this,
662 Uri.withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY),
664 mOrgEntries.add(entry);
668 entry = EditEntry.newNotesEntry(EditContactActivity.this, null, mUri);
669 mNoteEntries.add(entry);
674 // Rebuild the views if needed
677 mContactChanged = true;
679 View dataView = entry.view.findViewById(R.id.data);
680 if (dataView == null) {
681 entry.view.requestFocus();
683 dataView.requestFocus();
688 private void doRevertAction() {
692 private void doPickPhotoAction() {
693 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
694 // TODO: get these values from constants somewhere
695 intent.setType("image/*");
696 intent.putExtra("crop", "true");
697 intent.putExtra("aspectX", 1);
698 intent.putExtra("aspectY", 1);
699 intent.putExtra("outputX", 96);
700 intent.putExtra("outputY", 96);
702 intent.putExtra("return-data", true);
703 startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
704 } catch (ActivityNotFoundException e) {
705 new AlertDialog.Builder(EditContactActivity.this)
706 .setTitle(R.string.errorDialogTitle)
707 .setMessage(R.string.photoPickerNotFoundText)
708 .setPositiveButton(android.R.string.ok, null)
713 private void doRemovePhotoAction() {
715 mPhotoChanged = true;
716 setPhotoPresent(false);
719 private void doPickRingtone(EditEntry entry) {
720 Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
721 // Allow user to pick 'Default'
722 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
723 // Show only ringtones
724 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
725 // Don't show 'Silent'
726 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
729 if (entry.data != null) {
730 ringtoneUri = Uri.parse(entry.data);
732 // Otherwise pick default ringtone Uri so that something is selected.
733 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
736 // Put checkmark next to the current ringtone for this contact
737 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
739 startActivityForResult(intent, RINGTONE_PICKED);
742 private void handleRingtonePicked(Uri pickedUri) {
743 EditEntry entry = getOtherEntry(People.CUSTOM_RINGTONE);
745 Log.w(TAG, "Ringtone picked but could not find ringtone entry");
749 if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
752 entry.data = pickedUri.toString();
755 updateRingtoneView(entry);
758 private void updateRingtoneView(EditEntry entry) {
760 if (entry.data == null) {
761 ringtoneName = getString(R.string.default_ringtone);
763 Uri ringtoneUri = Uri.parse(entry.data);
764 Ringtone ringtone = RingtoneManager.getRingtone(this, ringtoneUri);
765 if (ringtone == null) {
766 Log.w(TAG, "ringtone's URI doesn't resolve to a Ringtone");
769 ringtoneName = ringtone.getTitle(this);
772 updateDataView(entry, ringtoneName);
775 private void updateDataView(EditEntry entry, String text) {
776 TextView dataView = (TextView) entry.view.findViewById(R.id.data);
777 dataView.setText(text);
781 protected Dialog onCreateDialog(int id) {
783 case DELETE_CONFIRMATION_DIALOG:
784 return new AlertDialog.Builder(EditContactActivity.this)
785 .setTitle(R.string.deleteConfirmation_title)
786 .setIcon(android.R.drawable.ic_dialog_alert)
787 .setMessage(R.string.deleteConfirmation)
788 .setNegativeButton(android.R.string.cancel, null)
789 .setPositiveButton(android.R.string.ok, mDeleteContactDialogListener)
790 .setCancelable(false)
793 return super.onCreateDialog(id);
796 static String[] getLabelsForKind(Context context, int kind) {
797 final Resources resources = context.getResources();
799 case Contacts.KIND_PHONE:
800 return resources.getStringArray(android.R.array.phoneTypes);
801 case Contacts.KIND_EMAIL:
802 return resources.getStringArray(android.R.array.emailAddressTypes);
803 case Contacts.KIND_POSTAL:
804 return resources.getStringArray(android.R.array.postalAddressTypes);
805 case Contacts.KIND_IM:
806 return resources.getStringArray(android.R.array.imProtocols);
807 case Contacts.KIND_ORGANIZATION:
808 return resources.getStringArray(android.R.array.organizationTypes);
809 case EditEntry.KIND_CONTACT:
810 return resources.getStringArray(R.array.otherLabels);
815 int getTypeFromLabelPosition(CharSequence[] labels, int labelPosition) {
816 // In the UI Custom... comes last, but it is uses the constant 0
817 // so it is in the same location across the various kinds. Fix up the
818 // position to a valid type here.
819 if (labelPosition == labels.length - 1) {
820 return ContactMethods.TYPE_CUSTOM;
822 return labelPosition + 1;
826 private EditEntry getOtherEntry(String column) {
827 for (int i = mOtherEntries.size() - 1; i >= 0; i--) {
828 EditEntry entry = mOtherEntries.get(i);
829 if (isOtherEntry(entry, column)) {
836 private static boolean isOtherEntry(EditEntry entry, String column) {
837 return entry != null && entry.column != null && entry.column.equals(column);
840 private void createCustomPicker(final EditEntry entry, final ArrayList<EditEntry> addTo) {
841 final EditText label = new EditText(this);
842 label.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS));
843 label.requestFocus();
844 new AlertDialog.Builder(this)
846 .setTitle(R.string.customLabelPickerTitle)
847 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
848 public void onClick(DialogInterface dialog, int which) {
849 entry.setLabel(EditContactActivity.this, ContactMethods.TYPE_CUSTOM,
850 label.getText().toString());
851 mContactChanged = true;
856 entry.view.requestFocus(View.FOCUS_DOWN);
860 .setNegativeButton(android.R.string.cancel, null)
865 * Saves or creates the contact based on the mode, and if sucessful finishes the activity.
867 private void doSaveAction() {
868 // Save or create the contact if needed
879 Log.e(TAG, "Unknown state in doSaveOrCreate: " + mState);
886 * Save the various fields to the existing contact.
888 private void save() {
889 ContentValues values = new ContentValues();
893 // Handle the name and send to voicemail specially
894 final String name = mNameView.getText().toString();
895 if (name != null && TextUtils.isGraphic(name)) {
898 values.put(People.NAME, name);
899 values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
900 mResolver.update(mUri, values, null, null);
903 // Only write the photo if it's changed, since we don't initially load mPhoto
904 if (mPhoto != null) {
905 ByteArrayOutputStream stream = new ByteArrayOutputStream();
906 mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
907 Contacts.People.setPhotoData(mResolver, mUri, stream.toByteArray());
909 Contacts.People.setPhotoData(mResolver, mUri, null);
913 int entryCount = ContactEntryAdapter.countEntries(mSections, false);
914 for (int i = 0; i < entryCount; i++) {
915 EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
916 int kind = entry.kind;
917 data = entry.getData();
918 boolean empty = data == null || !TextUtils.isGraphic(data);
919 if (kind == EditEntry.KIND_CONTACT) {
922 values.put(entry.column, data);
923 mResolver.update(entry.uri, values, null, null);
926 values.put(entry.column, (String) null);
927 mResolver.update(entry.uri, values, null, null);
932 entry.toValues(values);
934 mResolver.update(entry.uri, values, null, null);
936 mResolver.insert(entry.uri, values);
939 } else if (entry.id != 0) {
940 mResolver.delete(entry.uri, null, null);
945 if (numValues == 0) {
946 // The contact is completely empty, delete it
947 mResolver.delete(mUri, null, null);
949 setResult(RESULT_CANCELED);
951 // Add the entry to the my contacts group if it isn't there already
952 People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
953 setResult(RESULT_OK, new Intent().setData(mUri));
955 // Only notify user if we actually changed contact
956 if (mContactChanged || mPhotoChanged) {
957 Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
963 * Takes the entered data and saves it to a new contact.
965 private void create() {
966 ContentValues values = new ContentValues();
970 // Create the contact itself
971 final String name = mNameView.getText().toString();
972 if (name != null && TextUtils.isGraphic(name)) {
975 values.put(People.NAME, name);
976 values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
978 // Add the contact to the My Contacts group
979 Uri contactUri = People.createPersonInMyContactsGroup(mResolver, values);
981 // Add the contact to the group that is being displayed in the contact list
982 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
983 int displayType = prefs.getInt(ContactsListActivity.PREF_DISPLAY_TYPE,
984 ContactsListActivity.DISPLAY_TYPE_UNKNOWN);
985 if (displayType == ContactsListActivity.DISPLAY_TYPE_USER_GROUP) {
986 String displayGroup = prefs.getString(ContactsListActivity.PREF_DISPLAY_INFO,
988 if (!TextUtils.isEmpty(displayGroup)) {
989 People.addToGroup(mResolver, ContentUris.parseId(contactUri), displayGroup);
992 // Check to see if we're not syncing everything and if so if My Contacts is synced.
993 // If it isn't then the created contact can end up not in any groups that are
994 // currently synced and end up getting removed from the phone, which is really bad.
995 boolean syncingEverything = !"0".equals(Contacts.Settings.getSetting(mResolver, null,
996 Contacts.Settings.SYNC_EVERYTHING));
997 if (!syncingEverything) {
998 boolean syncingMyContacts = false;
999 Cursor c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups.SHOULD_SYNC },
1000 Groups.SYSTEM_ID + "=?", new String[] { Groups.GROUP_MY_CONTACTS }, null);
1003 if (c.moveToFirst()) {
1004 syncingMyContacts = !"0".equals(c.getString(0));
1011 if (!syncingMyContacts) {
1012 // Not syncing My Contacts, so find a group that is being synced and stick
1013 // the contact in there. We sort the list so at least all contacts
1014 // will appear in the same group.
1015 c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups._ID },
1016 Groups.SHOULD_SYNC + "!=0", null, Groups.DEFAULT_SORT_ORDER);
1019 if (c.moveToFirst()) {
1020 People.addToGroup(mResolver, ContentUris.parseId(contactUri),
1032 if (mPhoto != null) {
1033 ByteArrayOutputStream stream = new ByteArrayOutputStream();
1034 mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
1035 Contacts.People.setPhotoData(getContentResolver(), contactUri, stream.toByteArray());
1038 // Create the contact methods
1039 int entryCount = ContactEntryAdapter.countEntries(mSections, false);
1040 for (int i = 0; i < entryCount; i++) {
1041 EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
1042 if (entry.kind != EditEntry.KIND_CONTACT) {
1044 if (entry.toValues(values)) {
1045 // Only create the entry if there is data
1046 entry.uri = mResolver.insert(
1047 Uri.withAppendedPath(contactUri, entry.contentDirectory), values);
1048 entry.id = ContentUris.parseId(entry.uri);
1052 // Update the contact with any straggling data, like notes
1053 data = entry.getData();
1055 if (data != null && TextUtils.isGraphic(data)) {
1056 values.put(entry.column, data);
1057 mResolver.update(contactUri, values, null, null);
1063 if (numValues == 0) {
1064 mResolver.delete(contactUri, null, null);
1065 setResult(RESULT_CANCELED);
1068 Intent resultIntent = new Intent()
1070 .putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1071 setResult(RESULT_OK, resultIntent);
1072 Toast.makeText(this, R.string.contactCreatedToast, Toast.LENGTH_SHORT).show();
1077 * Build up the entries to display on the screen.
1079 * @param extras the extras used to start this activity, may be null
1081 private void buildEntriesForEdit(Bundle extras) {
1082 Cursor personCursor = mResolver.query(mUri, CONTACT_PROJECTION, null, null, null);
1083 if (personCursor == null) {
1084 Log.e(TAG, "invalid contact uri: " + mUri);
1087 } else if (!personCursor.moveToFirst()) {
1088 Log.e(TAG, "invalid contact uri: " + mUri);
1090 personCursor.close();
1094 // Clear out the old entries
1095 int numSections = mSections.size();
1096 for (int i = 0; i < numSections; i++) {
1097 mSections.get(i).clear();
1103 mNameView.setText(personCursor.getString(CONTACT_NAME_COLUMN));
1104 mNameView.addTextChangedListener(this);
1107 mPhoto = People.loadContactPhoto(this, mUri, 0, null);
1108 if (mPhoto == null) {
1109 setPhotoPresent(false);
1111 setPhotoPresent(true);
1112 mPhotoImageView.setImageBitmap(mPhoto);
1116 Uri organizationsUri = Uri.withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY);
1117 Cursor organizationsCursor = mResolver.query(organizationsUri, ORGANIZATIONS_PROJECTION,
1120 if (organizationsCursor != null) {
1121 while (organizationsCursor.moveToNext()) {
1122 int type = organizationsCursor.getInt(ORGANIZATIONS_TYPE_COLUMN);
1123 String label = organizationsCursor.getString(ORGANIZATIONS_LABEL_COLUMN);
1124 String company = organizationsCursor.getString(ORGANIZATIONS_COMPANY_COLUMN);
1125 String title = organizationsCursor.getString(ORGANIZATIONS_TITLE_COLUMN);
1126 long id = organizationsCursor.getLong(ORGANIZATIONS_ID_COLUMN);
1127 Uri uri = ContentUris.withAppendedId(Organizations.CONTENT_URI, id);
1129 // Add an organization entry
1130 entry = EditEntry.newOrganizationEntry(this, label, type, company, title, uri, id);
1131 entry.isPrimary = organizationsCursor.getLong(ORGANIZATIONS_ISPRIMARY_COLUMN) != 0;
1132 mOrgEntries.add(entry);
1134 organizationsCursor.close();
1138 if (!personCursor.isNull(CONTACT_NOTES_COLUMN)) {
1139 entry = EditEntry.newNotesEntry(this, personCursor.getString(CONTACT_NOTES_COLUMN),
1141 mNoteEntries.add(entry);
1145 entry = EditEntry.newRingtoneEntry(this,
1146 personCursor.getString(CONTACT_CUSTOM_RINGTONE_COLUMN), mUri);
1147 mOtherEntries.add(entry);
1149 // Send to voicemail
1150 entry = EditEntry.newSendToVoicemailEntry(this,
1151 personCursor.getString(CONTACT_SEND_TO_VOICEMAIL_COLUMN), mUri);
1152 mOtherEntries.add(entry);
1155 mPhoneticNameView.setText(personCursor.getString(CONTACT_PHONETIC_NAME_COLUMN));
1156 mPhoneticNameView.addTextChangedListener(this);
1158 personCursor.close();
1160 // Build up the phone entries
1161 Uri phonesUri = Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY);
1162 Cursor phonesCursor = mResolver.query(phonesUri, PHONES_PROJECTION,
1165 if (phonesCursor != null) {
1166 while (phonesCursor.moveToNext()) {
1167 int type = phonesCursor.getInt(PHONES_TYPE_COLUMN);
1168 String label = phonesCursor.getString(PHONES_LABEL_COLUMN);
1169 String number = phonesCursor.getString(PHONES_NUMBER_COLUMN);
1170 long id = phonesCursor.getLong(PHONES_ID_COLUMN);
1171 boolean isPrimary = phonesCursor.getLong(PHONES_ISPRIMARY_COLUMN) != 0;
1172 Uri uri = ContentUris.withAppendedId(phonesUri, id);
1174 // Add a phone number entry
1175 entry = EditEntry.newPhoneEntry(this, label, type, number, uri, id);
1176 entry.isPrimary = isPrimary;
1177 mPhoneEntries.add(entry);
1179 // Keep track of which primary types have been added
1180 if (type == Phones.TYPE_MOBILE) {
1181 mMobilePhoneAdded = true;
1185 phonesCursor.close();
1188 // Build the contact method entries
1189 Uri methodsUri = Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY);
1190 Cursor methodsCursor = mResolver.query(methodsUri, METHODS_PROJECTION, null, null, null);
1192 if (methodsCursor != null) {
1193 while (methodsCursor.moveToNext()) {
1194 int kind = methodsCursor.getInt(METHODS_KIND_COLUMN);
1195 String label = methodsCursor.getString(METHODS_LABEL_COLUMN);
1196 String data = methodsCursor.getString(METHODS_DATA_COLUMN);
1197 String auxData = methodsCursor.getString(METHODS_AUX_DATA_COLUMN);
1198 int type = methodsCursor.getInt(METHODS_TYPE_COLUMN);
1199 long id = methodsCursor.getLong(METHODS_ID_COLUMN);
1200 boolean isPrimary = methodsCursor.getLong(METHODS_ISPRIMARY_COLUMN) != 0;
1201 Uri uri = ContentUris.withAppendedId(methodsUri, id);
1204 case Contacts.KIND_EMAIL: {
1205 entry = EditEntry.newEmailEntry(this, label, type, data, uri, id);
1206 entry.isPrimary = isPrimary;
1207 mEmailEntries.add(entry);
1210 mPrimaryEmailAdded = true;
1215 case Contacts.KIND_POSTAL: {
1216 entry = EditEntry.newPostalEntry(this, label, type, data, uri, id);
1217 entry.isPrimary = isPrimary;
1218 mPostalEntries.add(entry);
1222 case Contacts.KIND_IM: {
1223 Object protocolObj = ContactMethods.decodeImProtocol(auxData);
1224 if (protocolObj == null) {
1225 // Invalid IM protocol, log it then ignore.
1226 Log.e(TAG, "Couldn't decode IM protocol: " + auxData);
1229 if (protocolObj instanceof Number) {
1230 int protocol = ((Number) protocolObj).intValue();
1231 entry = EditEntry.newImEntry(this,
1232 getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
1235 entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, data,
1238 mImEntries.add(entry);
1245 methodsCursor.close();
1248 // Add values from the extras, if there are any
1249 if (extras != null) {
1250 addFromExtras(extras, phonesUri, methodsUri);
1253 // Add the base types if needed
1254 if (!mMobilePhoneAdded) {
1255 entry = EditEntry.newPhoneEntry(this,
1256 Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY),
1257 DEFAULT_PHONE_TYPE);
1258 mPhoneEntries.add(entry);
1261 if (!mPrimaryEmailAdded) {
1262 entry = EditEntry.newEmailEntry(this,
1263 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
1264 DEFAULT_EMAIL_TYPE);
1265 entry.isPrimary = true;
1266 mEmailEntries.add(entry);
1269 mContactChanged = false;
1273 * Build the list of EditEntries for full mode insertions.
1275 * @param extras the extras used to start this activity, may be null
1277 private void buildEntriesForInsert(Bundle extras) {
1278 // Clear out the old entries
1279 int numSections = mSections.size();
1280 for (int i = 0; i < numSections; i++) {
1281 mSections.get(i).clear();
1286 // Check the intent extras
1287 if (extras != null) {
1288 addFromExtras(extras, null, null);
1292 mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
1294 // Add the base entries if they're not already present
1295 if (!mMobilePhoneAdded) {
1296 entry = EditEntry.newPhoneEntry(this, null, Phones.TYPE_MOBILE);
1297 entry.isPrimary = true;
1298 mPhoneEntries.add(entry);
1301 if (!mPrimaryEmailAdded) {
1302 entry = EditEntry.newEmailEntry(this, null, DEFAULT_EMAIL_TYPE);
1303 entry.isPrimary = true;
1304 mEmailEntries.add(entry);
1308 entry = EditEntry.newRingtoneEntry(this, null, mUri);
1309 mOtherEntries.add(entry);
1311 // Send to voicemail
1312 entry = EditEntry.newSendToVoicemailEntry(this, "0", mUri);
1313 mOtherEntries.add(entry);
1316 private void addFromExtras(Bundle extras, Uri phonesUri, Uri methodsUri) {
1319 // Read the name from the bundle
1320 CharSequence name = extras.getCharSequence(Insert.NAME);
1321 if (name != null && TextUtils.isGraphic(name)) {
1322 mNameView.setText(name);
1325 // Read the phonetic name from the bundle
1326 CharSequence phoneticName = extras.getCharSequence(Insert.PHONETIC_NAME);
1327 if (!TextUtils.isEmpty(phoneticName)) {
1328 mPhoneticNameView.setText(phoneticName);
1331 // Postal entries from extras
1332 CharSequence postal = extras.getCharSequence(Insert.POSTAL);
1333 int postalType = extras.getInt(Insert.POSTAL_TYPE, INVALID_TYPE);
1334 if (!TextUtils.isEmpty(postal) && postalType == INVALID_TYPE) {
1335 postalType = DEFAULT_POSTAL_TYPE;
1338 if (postalType != INVALID_TYPE) {
1339 entry = EditEntry.newPostalEntry(this, null, postalType, postal.toString(),
1341 entry.isPrimary = extras.getBoolean(Insert.POSTAL_ISPRIMARY);
1342 mPostalEntries.add(entry);
1345 // Email entries from extras
1346 addEmailFromExtras(extras, methodsUri, Insert.EMAIL, Insert.EMAIL_TYPE,
1347 Insert.EMAIL_ISPRIMARY);
1348 addEmailFromExtras(extras, methodsUri, Insert.SECONDARY_EMAIL, Insert.SECONDARY_EMAIL_TYPE,
1350 addEmailFromExtras(extras, methodsUri, Insert.TERTIARY_EMAIL, Insert.TERTIARY_EMAIL_TYPE,
1353 // Phone entries from extras
1354 addPhoneFromExtras(extras, phonesUri, Insert.PHONE, Insert.PHONE_TYPE,
1355 Insert.PHONE_ISPRIMARY);
1356 addPhoneFromExtras(extras, phonesUri, Insert.SECONDARY_PHONE, Insert.SECONDARY_PHONE_TYPE,
1358 addPhoneFromExtras(extras, phonesUri, Insert.TERTIARY_PHONE, Insert.TERTIARY_PHONE_TYPE,
1361 // IM entries from extras
1362 CharSequence imHandle = extras.getCharSequence(Insert.IM_HANDLE);
1363 CharSequence imProtocol = extras.getCharSequence(Insert.IM_PROTOCOL);
1365 if (imHandle != null && imProtocol != null) {
1366 Object protocolObj = ContactMethods.decodeImProtocol(imProtocol.toString());
1367 if (protocolObj instanceof Number) {
1368 int protocol = ((Number) protocolObj).intValue();
1369 entry = EditEntry.newImEntry(this,
1370 getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
1371 imHandle.toString(), methodsUri, 0);
1373 entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, imHandle.toString(),
1376 entry.isPrimary = extras.getBoolean(Insert.IM_ISPRIMARY);
1377 mImEntries.add(entry);
1381 private void addEmailFromExtras(Bundle extras, Uri methodsUri, String emailField,
1382 String typeField, String primaryField) {
1383 CharSequence email = extras.getCharSequence(emailField);
1385 // Correctly handle String in typeField as TYPE_CUSTOM
1386 int emailType = INVALID_TYPE;
1387 String customLabel = null;
1388 if(extras.get(typeField) instanceof String) {
1389 emailType = ContactMethods.TYPE_CUSTOM;
1390 customLabel = extras.getString(typeField);
1392 emailType = extras.getInt(typeField, INVALID_TYPE);
1395 if (!TextUtils.isEmpty(email) && emailType == INVALID_TYPE) {
1396 emailType = DEFAULT_EMAIL_TYPE;
1397 mPrimaryEmailAdded = true;
1400 if (emailType != INVALID_TYPE) {
1401 EditEntry entry = EditEntry.newEmailEntry(this, customLabel, emailType, email.toString(),
1403 entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
1404 mEmailEntries.add(entry);
1406 // Keep track of which primary types have been added
1407 if (entry.isPrimary) {
1408 mPrimaryEmailAdded = true;
1413 private void addPhoneFromExtras(Bundle extras, Uri phonesUri, String phoneField,
1414 String typeField, String primaryField) {
1415 CharSequence phoneNumber = extras.getCharSequence(phoneField);
1417 // Correctly handle String in typeField as TYPE_CUSTOM
1418 int phoneType = INVALID_TYPE;
1419 String customLabel = null;
1420 if(extras.get(typeField) instanceof String) {
1421 phoneType = Phones.TYPE_CUSTOM;
1422 customLabel = extras.getString(typeField);
1424 phoneType = extras.getInt(typeField, INVALID_TYPE);
1427 if (!TextUtils.isEmpty(phoneNumber) && phoneType == INVALID_TYPE) {
1428 phoneType = DEFAULT_PHONE_TYPE;
1431 if (phoneType != INVALID_TYPE) {
1432 EditEntry entry = EditEntry.newPhoneEntry(this, customLabel, phoneType,
1433 phoneNumber.toString(), phonesUri, 0);
1434 entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
1435 mPhoneEntries.add(entry);
1437 // Keep track of which primary types have been added
1438 if (phoneType == Phones.TYPE_MOBILE) {
1439 mMobilePhoneAdded = true;
1445 * Removes all existing views, builds new ones for all the entries, and adds them.
1447 private void buildViews() {
1448 // Remove existing views
1449 final LinearLayout layout = mLayout;
1450 layout.removeAllViews();
1452 buildViewsForSection(layout, mPhoneEntries,
1453 R.string.listSeparatorCallNumber_edit, SECTION_PHONES);
1454 buildViewsForSection(layout, mEmailEntries,
1455 R.string.listSeparatorSendEmail_edit, SECTION_EMAIL);
1456 buildViewsForSection(layout, mImEntries,
1457 R.string.listSeparatorSendIm_edit, SECTION_IM);
1458 buildViewsForSection(layout, mPostalEntries,
1459 R.string.listSeparatorMapAddress_edit, SECTION_POSTAL);
1460 buildViewsForSection(layout, mOrgEntries,
1461 R.string.listSeparatorOrganizations, SECTION_ORG);
1462 buildViewsForSection(layout, mNoteEntries,
1463 R.string.label_notes, SECTION_NOTE);
1465 buildOtherViews(layout, mOtherEntries);
1470 * Builds the views for a specific section.
1472 * @param layout the container
1473 * @param section the section to build the views for
1475 private void buildViewsForSection(final LinearLayout layout, ArrayList<EditEntry> section,
1476 int separatorResource, int sectionType) {
1478 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1479 layout.addView(divider);
1481 // Count up undeleted children
1482 int activeChildren = 0;
1483 for (int i = section.size() - 1; i >= 0; i--) {
1484 EditEntry entry = section.get(i);
1485 if (!entry.isDeleted) {
1490 // Build the correct group header based on undeleted children
1492 if (activeChildren == 0) {
1493 header = (ViewGroup) mInflater.inflate(R.layout.edit_separator_alone, layout, false);
1495 header = (ViewGroup) mInflater.inflate(R.layout.edit_separator, layout, false);
1498 // Because we're emulating a ListView, we need to handle focus changes
1499 // with some additional logic.
1500 header.setOnFocusChangeListener(this);
1502 TextView text = (TextView) header.findViewById(R.id.text);
1503 text.setText(getText(separatorResource));
1505 // Force TextView to always default color if we have children. This makes sure
1506 // we don't change color when parent is pressed.
1507 if (activeChildren > 0) {
1508 ColorStateList stateList = text.getTextColors();
1509 text.setTextColor(stateList.getDefaultColor());
1512 View addView = header.findViewById(R.id.separator);
1513 addView.setTag(Integer.valueOf(sectionType));
1514 addView.setOnClickListener(this);
1516 // Build views for the current section
1517 for (EditEntry entry : section) {
1518 entry.activity = this; // this could be null from when the state is restored
1519 if (!entry.isDeleted) {
1520 View view = buildViewForEntry(entry);
1521 header.addView(view);
1525 layout.addView(header);
1528 private void buildOtherViews(final LinearLayout layout, ArrayList<EditEntry> section) {
1529 // Build views for the current section, putting a divider between each one
1530 for (EditEntry entry : section) {
1531 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1532 layout.addView(divider);
1534 entry.activity = this; // this could be null from when the state is restored
1535 View view = buildViewForEntry(entry);
1536 view.setOnClickListener(this);
1537 layout.addView(view);
1540 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1541 layout.addView(divider);
1545 * Builds a view to display an EditEntry.
1547 * @param entry the entry to display
1548 * @return a view that will display the given entry
1550 /* package */ View buildViewForEntry(final EditEntry entry) {
1551 // Look for any existing entered text, and save it if found
1552 if (entry.view != null && entry.syncDataWithView) {
1553 String enteredText = ((TextView) entry.view.findViewById(R.id.data))
1554 .getText().toString();
1555 if (!TextUtils.isEmpty(enteredText)) {
1556 entry.data = enteredText;
1561 final ViewGroup parent = mLayout;
1564 // Because we're emulating a ListView, we might need to handle focus changes
1565 // with some additional logic.
1566 if (entry.kind == Contacts.KIND_ORGANIZATION) {
1567 view = mInflater.inflate(R.layout.edit_contact_entry_org, parent, false);
1568 } else if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
1569 view = mInflater.inflate(R.layout.edit_contact_entry_ringtone, parent, false);
1570 view.setOnFocusChangeListener(this);
1571 } else if (isOtherEntry(entry, People.SEND_TO_VOICEMAIL)) {
1572 view = mInflater.inflate(R.layout.edit_contact_entry_voicemail, parent, false);
1573 view.setOnFocusChangeListener(this);
1574 } else if (!entry.isStaticLabel) {
1575 view = mInflater.inflate(R.layout.edit_contact_entry, parent, false);
1577 view = mInflater.inflate(R.layout.edit_contact_entry_static_label, parent, false);
1581 // Set the entry as the tag so we can find it again later given just the view
1585 entry.bindLabel(this);
1588 TextView data = (TextView) view.findViewById(R.id.data);
1589 TextView data2 = (TextView) view.findViewById(R.id.data2);
1591 if (data instanceof Button) {
1592 data.setOnClickListener(this);
1594 if (data.length() == 0) {
1595 if (entry.syncDataWithView) {
1596 // If there is already data entered don't overwrite it
1597 data.setText(entry.data);
1599 fillViewData(entry);
1602 if (data2 != null && data2.length() == 0) {
1603 // If there is already data entered don't overwrite it
1604 data2.setText(entry.data2);
1606 data.setHint(entry.hint);
1607 if (data2 != null) data2.setHint(entry.hint2);
1608 if (entry.lines > 1) {
1609 data.setLines(entry.lines);
1610 data.setMaxLines(entry.maxLines);
1611 if (data2 != null) {
1612 data2.setLines(entry.lines);
1613 data2.setMaxLines(entry.maxLines);
1616 int contentType = entry.contentType;
1617 if (contentType != EditorInfo.TYPE_NULL) {
1618 data.setInputType(contentType);
1619 if (data2 != null) {
1620 data2.setInputType(contentType);
1622 if ((contentType&EditorInfo.TYPE_MASK_CLASS)
1623 == EditorInfo.TYPE_CLASS_PHONE) {
1624 data.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
1625 if (data2 != null) {
1626 data2.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
1631 // Give focus to children as requested, possibly after a configuration change
1632 View focusChild = view.findViewById(entry.requestFocusId);
1633 if (focusChild != null) {
1634 focusChild.requestFocus();
1635 if (focusChild instanceof EditText) {
1636 ((EditText) focusChild).setSelection(entry.requestCursor);
1640 // Reset requested focus values
1641 entry.requestFocusId = View.NO_ID;
1642 entry.requestCursor = 0;
1644 // Connect listeners up to watch for changed values.
1645 if (data instanceof EditText) {
1646 data.addTextChangedListener(this);
1648 if (data2 instanceof EditText) {
1649 data2.addTextChangedListener(this);
1652 // Hook up the delete button
1653 View delete = view.findViewById(R.id.delete);
1654 if (delete != null) delete.setOnClickListener(this);
1659 private void fillViewData(final EditEntry entry) {
1660 if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
1661 updateRingtoneView(entry);
1662 } else if (isOtherEntry(entry, People.SEND_TO_VOICEMAIL)) {
1663 CheckBox checkBox = (CheckBox) entry.view.findViewById(R.id.checkbox);
1664 boolean sendToVoicemail = false;
1665 if (entry.data != null) {
1666 sendToVoicemail = (Integer.valueOf(entry.data) == 1);
1668 checkBox.setChecked(sendToVoicemail);
1673 * Handles the results from the label change picker.
1675 private final class LabelPickedListener implements DialogInterface.OnClickListener {
1679 public LabelPickedListener(EditEntry entry, String[] labels) {
1684 public void onClick(DialogInterface dialog, int which) {
1685 // TODO: Use a managed dialog
1686 if (mEntry.kind != Contacts.KIND_IM) {
1687 final int type = getTypeFromLabelPosition(mLabels, which);
1688 if (type == ContactMethods.TYPE_CUSTOM) {
1689 createCustomPicker(mEntry, null);
1691 mEntry.setLabel(EditContactActivity.this, type, mLabels[which]);
1692 mContactChanged = true;
1695 mEntry.setLabel(EditContactActivity.this, which, mLabels[which]);
1696 mContactChanged = true;
1702 * A basic structure with the data for a contact entry in the list.
1704 private static final class EditEntry extends ContactEntryAdapter.Entry implements Parcelable {
1705 // These aren't stuffed into the parcel
1706 public EditContactActivity activity;
1709 // These are stuffed into the parcel
1711 public String hint2;
1712 public String column;
1713 public String contentDirectory;
1714 public String data2;
1715 public int contentType;
1718 * If 0 or 1, setSingleLine will be called. If negative, setSingleLine
1719 * will not be called.
1721 public int lines = 1;
1722 public boolean isPrimary;
1723 public boolean isDeleted = false;
1724 public boolean isStaticLabel = false;
1725 public boolean syncDataWithView = true;
1728 * Request focus on the child of this {@link EditEntry} found using
1729 * {@link View#findViewById(int)}. This value should be reset to
1730 * {@link View#NO_ID} after each use.
1732 public int requestFocusId = View.NO_ID;
1735 * If the {@link #requestFocusId} is an {@link EditText}, this value
1736 * indicates the requested cursor position placement.
1738 public int requestCursor = 0;
1740 private EditEntry() {
1741 // only used by CREATOR
1744 public EditEntry(EditContactActivity activity) {
1745 this.activity = activity;
1748 public EditEntry(EditContactActivity activity, String label,
1749 int type, String data, Uri uri, long id) {
1750 this.activity = activity;
1751 this.isPrimary = false;
1759 public int describeContents() {
1763 public void writeToParcel(Parcel parcel, int flags) {
1764 // Make sure to read data from the input field, if anything is entered
1767 // Write in our own fields.
1768 parcel.writeString(hint);
1769 parcel.writeString(hint2);
1770 parcel.writeString(column);
1771 parcel.writeString(contentDirectory);
1772 parcel.writeString(data2);
1773 parcel.writeInt(contentType);
1774 parcel.writeInt(type);
1775 parcel.writeInt(lines);
1776 parcel.writeInt(isPrimary ? 1 : 0);
1777 parcel.writeInt(isDeleted ? 1 : 0);
1778 parcel.writeInt(isStaticLabel ? 1 : 0);
1779 parcel.writeInt(syncDataWithView ? 1 : 0);
1781 // Write in the fields from Entry
1782 super.writeToParcel(parcel);
1785 public static final Parcelable.Creator<EditEntry> CREATOR =
1786 new Parcelable.Creator<EditEntry>() {
1787 public EditEntry createFromParcel(Parcel in) {
1788 EditEntry entry = new EditEntry();
1790 // Read out our own fields
1791 entry.hint = in.readString();
1792 entry.hint2 = in.readString();
1793 entry.column = in.readString();
1794 entry.contentDirectory = in.readString();
1795 entry.data2 = in.readString();
1796 entry.contentType = in.readInt();
1797 entry.type = in.readInt();
1798 entry.lines = in.readInt();
1799 entry.isPrimary = in.readInt() == 1;
1800 entry.isDeleted = in.readInt() == 1;
1801 entry.isStaticLabel = in.readInt() == 1;
1802 entry.syncDataWithView = in.readInt() == 1;
1804 // Read out the fields from Entry
1805 entry.readFromParcel(in);
1810 public EditEntry[] newArray(int size) {
1811 return new EditEntry[size];
1815 public void setLabel(Context context, int typeIn, String labelIn) {
1823 public void bindLabel(Context context) {
1824 TextView v = (TextView) view.findViewById(R.id.label);
1825 if (isStaticLabel) {
1831 case Contacts.KIND_PHONE: {
1832 v.setText(Phones.getDisplayLabel(context, type, label));
1836 case Contacts.KIND_IM: {
1837 v.setText(getLabelsForKind(activity, kind)[type]);
1841 case Contacts.KIND_ORGANIZATION: {
1842 v.setText(Organizations.getDisplayLabel(activity, type, label));
1847 v.setText(Contacts.ContactMethods.getDisplayLabel(context, kind, type, label));
1848 if (kind == Contacts.KIND_POSTAL) {
1854 v.setOnClickListener(activity);
1858 * Returns the data for the entry
1859 * @return the data for the entry
1861 public String getData() {
1862 if (view != null && syncDataWithView) {
1863 CharSequence text = ((TextView) view.findViewById(R.id.data)).getText();
1865 return text.toString();
1870 return data.toString();
1877 * Dumps the entry into a HashMap suitable for passing to the database.
1879 * @param values the HashMap to fill in.
1880 * @return true if the value should be saved, false otherwise
1882 public boolean toValues(ContentValues values) {
1883 boolean success = false;
1884 String labelString = null;
1885 // Save the type and label
1887 // Read the possibly updated label from the text field
1888 labelString = ((TextView) view.findViewById(R.id.label)).getText().toString();
1891 case Contacts.KIND_PHONE:
1892 if (type != Phones.TYPE_CUSTOM) {
1895 values.put(Phones.LABEL, labelString);
1896 values.put(Phones.TYPE, type);
1899 case Contacts.KIND_EMAIL:
1900 if (type != ContactMethods.TYPE_CUSTOM) {
1903 values.put(ContactMethods.LABEL, labelString);
1904 values.put(ContactMethods.KIND, kind);
1905 values.put(ContactMethods.TYPE, type);
1908 case Contacts.KIND_IM:
1909 values.put(ContactMethods.KIND, kind);
1910 values.put(ContactMethods.TYPE, ContactMethods.TYPE_OTHER);
1911 values.putNull(ContactMethods.LABEL);
1913 values.put(ContactMethods.AUX_DATA,
1914 ContactMethods.encodePredefinedImProtocol(type));
1916 values.put(ContactMethods.AUX_DATA,
1917 ContactMethods.encodeCustomImProtocol(label.toString()));
1921 case Contacts.KIND_POSTAL:
1922 if (type != ContactMethods.TYPE_CUSTOM) {
1925 values.put(ContactMethods.LABEL, labelString);
1926 values.put(ContactMethods.KIND, kind);
1927 values.put(ContactMethods.TYPE, type);
1930 case Contacts.KIND_ORGANIZATION:
1931 if (type != ContactMethods.TYPE_CUSTOM) {
1934 values.put(ContactMethods.LABEL, labelString);
1935 values.put(ContactMethods.TYPE, type);
1938 // Read the possibly updated data from the text field
1939 data2 = ((TextView) view.findViewById(R.id.data2)).getText().toString();
1941 if (!TextUtils.isGraphic(data2)) {
1942 values.putNull(Organizations.TITLE);
1944 values.put(Organizations.TITLE, data2.toString());
1950 Log.w(TAG, "unknown kind " + kind);
1951 values.put(ContactMethods.LABEL, labelString);
1952 values.put(ContactMethods.KIND, kind);
1953 values.put(ContactMethods.TYPE, type);
1957 // Only set the ISPRIMARY flag if part of the incoming data. This is because the
1958 // ContentProvider will try finding a new primary when setting to false, meaning
1959 // it's possible to lose primary altogether as we walk down the list. If this editor
1960 // implements editing of primaries in the future, this will need to be revisited.
1962 values.put(ContactMethods.ISPRIMARY, 1);
1966 if (view != null && syncDataWithView) {
1967 // Read the possibly updated data from the text field
1968 data = ((TextView) view.findViewById(R.id.data)).getText().toString();
1970 if (!TextUtils.isGraphic(data)) {
1971 values.putNull(column);
1974 values.put(column, data.toString());
1980 * Create a new empty organization entry
1982 public static final EditEntry newOrganizationEntry(EditContactActivity activity,
1983 Uri uri, int type) {
1984 return newOrganizationEntry(activity, null, type, null, null, uri, 0);
1988 * Create a new company entry with the given data.
1990 public static final EditEntry newOrganizationEntry(EditContactActivity activity,
1991 String label, int type, String company, String title, Uri uri, long id) {
1992 EditEntry entry = new EditEntry(activity, label, type, company, uri, id);
1993 entry.hint = activity.getString(R.string.ghostData_company);
1994 entry.hint2 = activity.getString(R.string.ghostData_title);
1995 entry.data2 = title;
1996 entry.column = Organizations.COMPANY;
1997 entry.contentDirectory = Organizations.CONTENT_DIRECTORY;
1998 entry.kind = Contacts.KIND_ORGANIZATION;
1999 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2000 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
2005 * Create a new notes entry with the given data.
2007 public static final EditEntry newNotesEntry(EditContactActivity activity,
2008 String data, Uri uri) {
2009 EditEntry entry = new EditEntry(activity);
2010 entry.label = activity.getString(R.string.label_notes);
2011 entry.hint = activity.getString(R.string.ghostData_notes);
2014 entry.column = People.NOTES;
2015 entry.maxLines = 10;
2018 entry.kind = KIND_CONTACT;
2019 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2020 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
2021 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
2022 entry.isStaticLabel = true;
2027 * Create a new ringtone entry with the given data.
2029 public static final EditEntry newRingtoneEntry(EditContactActivity activity,
2030 String data, Uri uri) {
2031 EditEntry entry = new EditEntry(activity);
2032 entry.label = activity.getString(R.string.label_ringtone);
2035 entry.column = People.CUSTOM_RINGTONE;
2036 entry.kind = KIND_CONTACT;
2037 entry.isStaticLabel = true;
2038 entry.syncDataWithView = false;
2044 * Create a new send-to-voicemail entry with the given data.
2046 public static final EditEntry newSendToVoicemailEntry(EditContactActivity activity,
2047 String data, Uri uri) {
2048 EditEntry entry = new EditEntry(activity);
2049 entry.label = activity.getString(R.string.actionIncomingCall);
2052 entry.column = People.SEND_TO_VOICEMAIL;
2053 entry.kind = KIND_CONTACT;
2054 entry.isStaticLabel = true;
2055 entry.syncDataWithView = false;
2061 * Create a new empty email entry
2063 public static final EditEntry newPhoneEntry(EditContactActivity activity,
2064 Uri uri, int type) {
2065 return newPhoneEntry(activity, null, type, null, uri, 0);
2069 * Create a new phone entry with the given data.
2071 public static final EditEntry newPhoneEntry(EditContactActivity activity,
2072 String label, int type, String data, Uri uri,
2074 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
2075 entry.hint = activity.getString(R.string.ghostData_phone);
2076 entry.column = People.Phones.NUMBER;
2077 entry.contentDirectory = People.Phones.CONTENT_DIRECTORY;
2078 entry.kind = Contacts.KIND_PHONE;
2079 entry.contentType = EditorInfo.TYPE_CLASS_PHONE;
2084 * Create a new empty email entry
2086 public static final EditEntry newEmailEntry(EditContactActivity activity,
2087 Uri uri, int type) {
2088 return newEmailEntry(activity, null, type, null, uri, 0);
2092 * Create a new email entry with the given data.
2094 public static final EditEntry newEmailEntry(EditContactActivity activity,
2095 String label, int type, String data, Uri uri,
2097 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
2098 entry.hint = activity.getString(R.string.ghostData_email);
2099 entry.column = ContactMethods.DATA;
2100 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
2101 entry.kind = Contacts.KIND_EMAIL;
2102 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2103 | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
2108 * Create a new empty postal address entry
2110 public static final EditEntry newPostalEntry(EditContactActivity activity,
2111 Uri uri, int type) {
2112 return newPostalEntry(activity, null, type, null, uri, 0);
2116 * Create a new postal address entry with the given data.
2118 * @param label label for the item, from the db not the display label
2119 * @param type the type of postal address
2120 * @param data the starting data for the entry, may be null
2121 * @param uri the uri for the entry if it already exists, may be null
2122 * @param id the id for the entry if it already exists, 0 it it doesn't
2123 * @return the new EditEntry
2125 public static final EditEntry newPostalEntry(EditContactActivity activity,
2126 String label, int type, String data, Uri uri, long id) {
2127 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
2128 entry.hint = activity.getString(R.string.ghostData_postal);
2129 entry.column = ContactMethods.DATA;
2130 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
2131 entry.kind = Contacts.KIND_POSTAL;
2132 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2133 | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
2134 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
2135 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
2142 * Create a new IM address entry
2144 public static final EditEntry newImEntry(EditContactActivity activity,
2145 Uri uri, int type) {
2146 return newImEntry(activity, null, type, null, uri, 0);
2150 * Create a new IM address entry with the given data.
2152 * @param label label for the item, from the db not the display label
2153 * @param protocol the type used
2154 * @param data the starting data for the entry, may be null
2155 * @param uri the uri for the entry if it already exists, may be null
2156 * @param id the id for the entry if it already exists, 0 it it doesn't
2157 * @return the new EditEntry
2159 public static final EditEntry newImEntry(EditContactActivity activity,
2160 String label, int protocol, String data, Uri uri, long id) {
2161 EditEntry entry = new EditEntry(activity, label, protocol, data, uri, id);
2162 entry.hint = activity.getString(R.string.ghostData_im);
2163 entry.column = ContactMethods.DATA;
2164 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
2165 entry.kind = Contacts.KIND_IM;
2166 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2167 | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
2172 public void afterTextChanged(Editable s) {
2173 // Someone edited a text field, so assume this contact is changed
2174 mContactChanged = true;
2177 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2178 // Do nothing; editing handled by afterTextChanged()
2181 public void onTextChanged(CharSequence s, int start, int before, int count) {
2182 // Do nothing; editing handled by afterTextChanged()
2185 public void onFocusChange(View v, boolean hasFocus) {
2186 // Because we're emulating a ListView, we need to setSelected() for
2187 // views as they are focused.
2188 v.setSelected(hasFocus);