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.Organizations;
73 import android.provider.Contacts.People;
74 import android.provider.Contacts.Phones;
75 import android.telephony.PhoneNumberFormattingTextWatcher;
76 import android.text.Editable;
77 import android.text.TextUtils;
78 import android.text.TextWatcher;
79 import android.text.method.TextKeyListener;
80 import android.text.method.TextKeyListener.Capitalize;
81 import android.util.Log;
82 import android.util.SparseBooleanArray;
83 import android.view.KeyEvent;
84 import android.view.LayoutInflater;
85 import android.view.Menu;
86 import android.view.MenuItem;
87 import android.view.View;
88 import android.view.ViewGroup;
89 import android.view.ViewParent;
90 import android.view.inputmethod.EditorInfo;
91 import android.widget.Button;
92 import android.widget.CheckBox;
93 import android.widget.EditText;
94 import android.widget.ImageView;
95 import android.widget.LinearLayout;
96 import android.widget.TextView;
97 import android.widget.Toast;
99 import java.io.ByteArrayOutputStream;
100 import java.util.ArrayList;
103 * Activity for editing or inserting a contact. Note that if the contact data changes in the
104 * background while this activity is running, the updates will be overwritten.
106 public final class EditContactActivity extends Activity implements View.OnClickListener,
107 TextWatcher, View.OnFocusChangeListener {
108 private static final String TAG = "EditContactActivity";
110 private static final int STATE_UNKNOWN = 0;
111 /** Editing an existing contact */
112 private static final int STATE_EDIT = 1;
113 /** The full insert mode */
114 private static final int STATE_INSERT = 2;
116 /** The launch code when picking a photo and the raw data is returned */
117 private static final int PHOTO_PICKED_WITH_DATA = 3021;
119 /** The launch code when picking a ringtone */
120 private static final int RINGTONE_PICKED = 3023;
122 // These correspond to the string array in resources for picker "other" items
123 final static int OTHER_ORGANIZATION = 0;
124 final static int OTHER_NOTE = 1;
127 final static int DELETE_CONFIRMATION_DIALOG = 2;
130 final static int SECTION_PHONES = 3;
131 final static int SECTION_EMAIL = 4;
132 final static int SECTION_IM = 5;
133 final static int SECTION_POSTAL = 6;
134 final static int SECTION_ORG = 7;
135 final static int SECTION_NOTE = 8;
138 public static final int MENU_ITEM_SAVE = 1;
139 public static final int MENU_ITEM_DONT_SAVE = 2;
140 public static final int MENU_ITEM_DELETE = 3;
141 public static final int MENU_ITEM_PHOTO = 6;
143 /** Used to represent an invalid type for a contact entry */
144 private static final int INVALID_TYPE = -1;
146 /** The default type for a phone that is added via an intent */
147 private static final int DEFAULT_PHONE_TYPE = Phones.TYPE_MOBILE;
149 /** The default type for an email that is added via an intent */
150 private static final int DEFAULT_EMAIL_TYPE = ContactMethods.TYPE_HOME;
152 /** The default type for a postal address that is added via an intent */
153 private static final int DEFAULT_POSTAL_TYPE = ContactMethods.TYPE_HOME;
155 private int mState; // saved across instances
156 private boolean mInsert; // saved across instances
157 private Uri mUri; // saved across instances
158 /** In insert mode this is the photo */
159 private Bitmap mPhoto; // saved across instances
160 private boolean mPhotoChanged = false; // saved across instances
162 private EditText mNameView;
163 private ImageView mPhotoImageView;
164 private LinearLayout mLayout;
165 private LayoutInflater mInflater;
166 private MenuItem mPhotoMenuItem;
167 private boolean mPhotoPresent = false;
168 private EditText mPhoneticNameView; // invisible in some locales, but always present
170 /** Flag marking this contact as changed, meaning we should write changes back. */
171 private boolean mContactChanged = false;
173 // These are accessed by inner classes. They're package scoped to make access more efficient.
174 /* package */ ContentResolver mResolver;
175 /* package */ ArrayList<EditEntry> mPhoneEntries = new ArrayList<EditEntry>();
176 /* package */ ArrayList<EditEntry> mEmailEntries = new ArrayList<EditEntry>();
177 /* package */ ArrayList<EditEntry> mImEntries = new ArrayList<EditEntry>();
178 /* package */ ArrayList<EditEntry> mPostalEntries = new ArrayList<EditEntry>();
179 /* package */ ArrayList<EditEntry> mOrgEntries = new ArrayList<EditEntry>();
180 /* package */ ArrayList<EditEntry> mNoteEntries = new ArrayList<EditEntry>();
181 /* package */ ArrayList<EditEntry> mOtherEntries = new ArrayList<EditEntry>();
182 /* package */ ArrayList<ArrayList<EditEntry>> mSections = new ArrayList<ArrayList<EditEntry>>();
184 /* package */ static final int MSG_DELETE = 1;
185 /* package */ static final int MSG_CHANGE_LABEL = 2;
186 /* package */ static final int MSG_ADD_PHONE = 3;
187 /* package */ static final int MSG_ADD_EMAIL = 4;
188 /* package */ static final int MSG_ADD_POSTAL = 5;
190 private static final int[] TYPE_PRECEDENCE_PHONES = new int[] {
191 Phones.TYPE_MOBILE, Phones.TYPE_HOME, Phones.TYPE_WORK, Phones.TYPE_OTHER
193 private static final int[] TYPE_PRECEDENCE_METHODS = new int[] {
194 ContactMethods.TYPE_HOME, ContactMethods.TYPE_WORK, ContactMethods.TYPE_OTHER
196 private static final int[] TYPE_PRECEDENCE_IM = new int[] {
197 ContactMethods.PROTOCOL_GOOGLE_TALK, ContactMethods.PROTOCOL_AIM,
198 ContactMethods.PROTOCOL_MSN, ContactMethods.PROTOCOL_YAHOO,
199 ContactMethods.PROTOCOL_JABBER
201 private static final int[] TYPE_PRECEDENCE_ORG = new int[] {
202 Organizations.TYPE_WORK, Organizations.TYPE_OTHER
205 public void onClick(View v) {
207 case R.id.photoImage: {
212 case R.id.checkable: {
213 CheckBox checkBox = (CheckBox) v.findViewById(R.id.checkbox);
216 EditEntry entry = findEntryForView(v);
217 entry.data = checkBox.isChecked() ? "1" : "0";
219 mContactChanged = true;
223 case R.id.entry_ringtone: {
224 EditEntry entry = findEntryForView(v);
225 doPickRingtone(entry);
229 case R.id.separator: {
230 // Someone clicked on a section header, so handle add action
231 int sectionType = (Integer) v.getTag();
232 doAddAction(sectionType);
236 case R.id.saveButton:
240 case R.id.discardButton:
245 EditEntry entry = findEntryForView(v);
247 // Clear the text and hide the view so it gets saved properly
248 ((TextView) entry.view.findViewById(R.id.data)).setText(null);
249 entry.view.setVisibility(View.GONE);
250 entry.isDeleted = true;
253 // Force rebuild of views because section headers might need to change
259 EditEntry entry = findEntryForView(v);
261 String[] labels = getLabelsForKind(this, entry.kind);
262 LabelPickedListener listener = new LabelPickedListener(entry, labels);
263 new AlertDialog.Builder(EditContactActivity.this)
264 .setItems(labels, listener)
265 .setTitle(R.string.selectLabel)
273 private void setPhotoPresent(boolean present) {
274 mPhotoPresent = present;
276 // Correctly scale the contact photo if present, otherwise just center
277 // the photo placeholder icon.
279 mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
281 mPhotoImageView.setImageResource(R.drawable.ic_menu_add_picture);
282 mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER);
285 if (mPhotoMenuItem != null) {
287 mPhotoMenuItem.setTitle(R.string.removePicture);
288 mPhotoMenuItem.setIcon(android.R.drawable.ic_menu_delete);
290 mPhotoMenuItem.setTitle(R.string.addPicture);
291 mPhotoMenuItem.setIcon(R.drawable.ic_menu_add_picture);
296 private EditEntry findEntryForView(View v) {
297 // Try to find the entry for this view
298 EditEntry entry = null;
300 Object tag = v.getTag();
301 if (tag != null && tag instanceof EditEntry) {
302 entry = (EditEntry) tag;
305 ViewParent parent = v.getParent();
306 if (parent != null && parent instanceof View) {
316 private DialogInterface.OnClickListener mDeleteContactDialogListener =
317 new DialogInterface.OnClickListener() {
318 public void onClick(DialogInterface dialog, int button) {
319 mResolver.delete(mUri, null, null);
324 private boolean mMobilePhoneAdded = false;
325 private boolean mPrimaryEmailAdded = false;
328 protected void onCreate(Bundle icicle) {
329 super.onCreate(icicle);
331 mResolver = getContentResolver();
333 // Build the list of sections
337 setContentView(R.layout.edit_contact);
338 mLayout = (LinearLayout) findViewById(R.id.list);
339 mNameView = (EditText) findViewById(R.id.name);
340 mPhotoImageView = (ImageView) findViewById(R.id.photoImage);
341 mPhotoImageView.setOnClickListener(this);
342 mPhoneticNameView = (EditText) findViewById(R.id.phonetic_name);
344 // Setup the bottom buttons
345 View view = findViewById(R.id.saveButton);
346 view.setOnClickListener(this);
347 view = findViewById(R.id.discardButton);
348 view.setOnClickListener(this);
350 mInflater = getLayoutInflater();
352 // Resolve the intent
353 mState = STATE_UNKNOWN;
354 Intent intent = getIntent();
355 String action = intent.getAction();
356 mUri = intent.getData();
358 if (action.equals(Intent.ACTION_EDIT)) {
359 if (icicle == null) {
360 // Build the entries & views
361 buildEntriesForEdit(getIntent().getExtras());
364 setTitle(R.string.editContact_title_edit);
366 } else if (action.equals(Intent.ACTION_INSERT)) {
367 if (icicle == null) {
368 // Build the entries & views
369 buildEntriesForInsert(getIntent().getExtras());
372 setTitle(R.string.editContact_title_insert);
373 mState = STATE_INSERT;
378 if (mState == STATE_UNKNOWN) {
379 Log.e(TAG, "Cannot resolve intent: " + intent);
384 if (mState == STATE_EDIT) {
385 setTitle(getResources().getText(R.string.editContact_title_edit));
387 setTitle(getResources().getText(R.string.editContact_title_insert));
391 private void setupSections() {
392 mSections.add(mPhoneEntries);
393 mSections.add(mEmailEntries);
394 mSections.add(mImEntries);
395 mSections.add(mPostalEntries);
396 mSections.add(mOrgEntries);
397 mSections.add(mNoteEntries);
398 mSections.add(mOtherEntries);
402 protected void onSaveInstanceState(Bundle outState) {
403 outState.putParcelableArrayList("phoneEntries", mPhoneEntries);
404 outState.putParcelableArrayList("emailEntries", mEmailEntries);
405 outState.putParcelableArrayList("imEntries", mImEntries);
406 outState.putParcelableArrayList("postalEntries", mPostalEntries);
407 outState.putParcelableArrayList("orgEntries", mOrgEntries);
408 outState.putParcelableArrayList("noteEntries", mNoteEntries);
409 outState.putParcelableArrayList("otherEntries", mOtherEntries);
410 outState.putInt("state", mState);
411 outState.putBoolean("insert", mInsert);
412 outState.putParcelable("uri", mUri);
413 outState.putString("name", mNameView.getText().toString());
414 outState.putParcelable("photo", mPhoto);
415 outState.putBoolean("photoChanged", mPhotoChanged);
416 outState.putString("phoneticName", mPhoneticNameView.getText().toString());
417 outState.putBoolean("contactChanged", mContactChanged);
421 protected void onRestoreInstanceState(Bundle inState) {
422 mPhoneEntries = inState.getParcelableArrayList("phoneEntries");
423 mEmailEntries = inState.getParcelableArrayList("emailEntries");
424 mImEntries = inState.getParcelableArrayList("imEntries");
425 mPostalEntries = inState.getParcelableArrayList("postalEntries");
426 mOrgEntries = inState.getParcelableArrayList("orgEntries");
427 mNoteEntries = inState.getParcelableArrayList("noteEntries");
428 mOtherEntries = inState.getParcelableArrayList("otherEntries");
431 mState = inState.getInt("state");
432 mInsert = inState.getBoolean("insert");
433 mUri = inState.getParcelable("uri");
434 mNameView.setText(inState.getString("name"));
435 mPhoto = inState.getParcelable("photo");
436 if (mPhoto != null) {
437 mPhotoImageView.setImageBitmap(mPhoto);
438 setPhotoPresent(true);
440 mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
441 setPhotoPresent(false);
443 mPhotoChanged = inState.getBoolean("photoChanged");
444 mPhoneticNameView.setText(inState.getString("phoneticName"));
445 mContactChanged = inState.getBoolean("contactChanged");
447 // Now that everything is restored, build the view
452 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
453 if (resultCode != RESULT_OK) {
457 switch (requestCode) {
458 case PHOTO_PICKED_WITH_DATA: {
459 final Bundle extras = data.getExtras();
460 if (extras != null) {
461 Bitmap photo = extras.getParcelable("data");
463 mPhotoChanged = true;
464 mPhotoImageView.setImageBitmap(photo);
465 setPhotoPresent(true);
470 case RINGTONE_PICKED: {
471 Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
472 handleRingtonePicked(pickedUri);
473 mContactChanged = true;
480 public boolean onKeyDown(int keyCode, KeyEvent event) {
482 case KeyEvent.KEYCODE_BACK: {
487 return super.onKeyDown(keyCode, event);
491 public boolean onCreateOptionsMenu(Menu menu) {
492 super.onCreateOptionsMenu(menu);
493 menu.add(0, MENU_ITEM_SAVE, 0, R.string.menu_done)
494 .setIcon(android.R.drawable.ic_menu_save)
495 .setAlphabeticShortcut('\n');
496 menu.add(0, MENU_ITEM_DONT_SAVE, 0, R.string.menu_doNotSave)
497 .setIcon(android.R.drawable.ic_menu_close_clear_cancel)
498 .setAlphabeticShortcut('q');
500 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
501 .setIcon(android.R.drawable.ic_menu_delete);
504 mPhotoMenuItem = menu.add(0, MENU_ITEM_PHOTO, 0, null);
505 // Updates the state of the menu item
506 setPhotoPresent(mPhotoPresent);
512 public boolean onOptionsItemSelected(MenuItem item) {
513 switch (item.getItemId()) {
518 case MENU_ITEM_DONT_SAVE:
522 case MENU_ITEM_DELETE:
524 showDialog(DELETE_CONFIRMATION_DIALOG);
527 case MENU_ITEM_PHOTO:
528 if (!mPhotoPresent) {
531 doRemovePhotoAction();
540 * Try guessing the next-best type of {@link EditEntry} to insert into the
541 * given list. We walk down the precedence list until we find a type that
542 * doesn't exist yet, or default to the lowest ranking type.
544 private int guessNextType(ArrayList<EditEntry> entries, int[] precedenceList) {
545 // Keep track of the types we've seen already
546 SparseBooleanArray existAlready = new SparseBooleanArray(entries.size());
547 for (int i = entries.size() - 1; i >= 0; i--) {
548 EditEntry entry = entries.get(i);
549 if (!entry.isDeleted) {
550 existAlready.put(entry.type, true);
554 // Pick the first item we haven't seen
555 for (int type : precedenceList) {
556 if (!existAlready.get(type, false)) {
561 // Otherwise default to last item
562 return precedenceList[precedenceList.length - 1];
565 private void doAddAction(int sectionType) {
566 EditEntry entry = null;
567 switch (sectionType) {
568 case SECTION_PHONES: {
569 // Try figuring out which type to insert next
570 int nextType = guessNextType(mPhoneEntries, TYPE_PRECEDENCE_PHONES);
571 entry = EditEntry.newPhoneEntry(EditContactActivity.this,
572 Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY),
574 mPhoneEntries.add(entry);
577 case SECTION_EMAIL: {
578 // Try figuring out which type to insert next
579 int nextType = guessNextType(mEmailEntries, TYPE_PRECEDENCE_METHODS);
580 entry = EditEntry.newEmailEntry(EditContactActivity.this,
581 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
583 mEmailEntries.add(entry);
587 // Try figuring out which type to insert next
588 int nextType = guessNextType(mImEntries, TYPE_PRECEDENCE_IM);
589 entry = EditEntry.newImEntry(EditContactActivity.this,
590 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
592 mImEntries.add(entry);
595 case SECTION_POSTAL: {
596 int nextType = guessNextType(mPostalEntries, TYPE_PRECEDENCE_METHODS);
597 entry = EditEntry.newPostalEntry(EditContactActivity.this,
598 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
600 mPostalEntries.add(entry);
604 int nextType = guessNextType(mOrgEntries, TYPE_PRECEDENCE_ORG);
605 entry = EditEntry.newOrganizationEntry(EditContactActivity.this,
606 Uri.withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY),
608 mOrgEntries.add(entry);
612 entry = EditEntry.newNotesEntry(EditContactActivity.this, null, mUri);
613 mNoteEntries.add(entry);
618 // Rebuild the views if needed
621 mContactChanged = true;
623 View dataView = entry.view.findViewById(R.id.data);
624 if (dataView == null) {
625 entry.view.requestFocus();
627 dataView.requestFocus();
632 private void doRevertAction() {
636 private void doPickPhotoAction() {
637 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
638 // TODO: get these values from constants somewhere
639 intent.setType("image/*");
640 intent.putExtra("crop", "true");
641 intent.putExtra("aspectX", 1);
642 intent.putExtra("aspectY", 1);
643 intent.putExtra("outputX", 96);
644 intent.putExtra("outputY", 96);
646 intent.putExtra("return-data", true);
647 startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
648 } catch (ActivityNotFoundException e) {
649 new AlertDialog.Builder(EditContactActivity.this)
650 .setTitle(R.string.errorDialogTitle)
651 .setMessage(R.string.photoPickerNotFoundText)
652 .setPositiveButton(android.R.string.ok, null)
657 private void doRemovePhotoAction() {
659 mPhotoChanged = true;
660 setPhotoPresent(false);
663 private void doPickRingtone(EditEntry entry) {
664 Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
665 // Allow user to pick 'Default'
666 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
667 // Show only ringtones
668 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
669 // Don't show 'Silent'
670 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
673 if (entry.data != null) {
674 ringtoneUri = Uri.parse(entry.data);
676 // Otherwise pick default ringtone Uri so that something is selected.
677 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
680 // Put checkmark next to the current ringtone for this contact
681 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
683 startActivityForResult(intent, RINGTONE_PICKED);
686 private void handleRingtonePicked(Uri pickedUri) {
687 EditEntry entry = getOtherEntry(People.CUSTOM_RINGTONE);
689 Log.w(TAG, "Ringtone picked but could not find ringtone entry");
693 if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
696 entry.data = pickedUri.toString();
699 updateRingtoneView(entry);
702 private void updateRingtoneView(EditEntry entry) {
704 if (entry.data == null) {
705 ringtoneName = getString(R.string.default_ringtone);
707 Uri ringtoneUri = Uri.parse(entry.data);
708 Ringtone ringtone = RingtoneManager.getRingtone(this, ringtoneUri);
709 if (ringtone == null) {
710 Log.w(TAG, "ringtone's URI doesn't resolve to a Ringtone");
713 ringtoneName = ringtone.getTitle(this);
716 updateDataView(entry, ringtoneName);
719 private void updateDataView(EditEntry entry, String text) {
720 TextView dataView = (TextView) entry.view.findViewById(R.id.data);
721 dataView.setText(text);
725 protected Dialog onCreateDialog(int id) {
727 case DELETE_CONFIRMATION_DIALOG:
728 return new AlertDialog.Builder(EditContactActivity.this)
729 .setTitle(R.string.deleteConfirmation_title)
730 .setIcon(android.R.drawable.ic_dialog_alert)
731 .setMessage(R.string.deleteConfirmation)
732 .setNegativeButton(android.R.string.cancel, null)
733 .setPositiveButton(android.R.string.ok, mDeleteContactDialogListener)
734 .setCancelable(false)
737 return super.onCreateDialog(id);
740 static String[] getLabelsForKind(Context context, int kind) {
741 final Resources resources = context.getResources();
743 case Contacts.KIND_PHONE:
744 return resources.getStringArray(android.R.array.phoneTypes);
745 case Contacts.KIND_EMAIL:
746 return resources.getStringArray(android.R.array.emailAddressTypes);
747 case Contacts.KIND_POSTAL:
748 return resources.getStringArray(android.R.array.postalAddressTypes);
749 case Contacts.KIND_IM:
750 return resources.getStringArray(android.R.array.imProtocols);
751 case Contacts.KIND_ORGANIZATION:
752 return resources.getStringArray(android.R.array.organizationTypes);
753 case EditEntry.KIND_CONTACT:
754 return resources.getStringArray(R.array.otherLabels);
759 int getTypeFromLabelPosition(CharSequence[] labels, int labelPosition) {
760 // In the UI Custom... comes last, but it is uses the constant 0
761 // so it is in the same location across the various kinds. Fix up the
762 // position to a valid type here.
763 if (labelPosition == labels.length - 1) {
764 return ContactMethods.TYPE_CUSTOM;
766 return labelPosition + 1;
770 private EditEntry getOtherEntry(String column) {
771 for (int i = mOtherEntries.size() - 1; i >= 0; i--) {
772 EditEntry entry = mOtherEntries.get(i);
773 if (isOtherEntry(entry, column)) {
780 private static boolean isOtherEntry(EditEntry entry, String column) {
781 return entry != null && entry.column != null && entry.column.equals(column);
784 private void createCustomPicker(final EditEntry entry, final ArrayList<EditEntry> addTo) {
785 final EditText label = new EditText(this);
786 label.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS));
787 label.requestFocus();
788 new AlertDialog.Builder(this)
790 .setTitle(R.string.customLabelPickerTitle)
791 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
792 public void onClick(DialogInterface dialog, int which) {
793 entry.setLabel(EditContactActivity.this, ContactMethods.TYPE_CUSTOM,
794 label.getText().toString());
795 mContactChanged = true;
800 entry.view.requestFocus(View.FOCUS_DOWN);
804 .setNegativeButton(android.R.string.cancel, null)
809 * Saves or creates the contact based on the mode, and if sucessful finishes the activity.
811 private void doSaveAction() {
812 // Save or create the contact if needed
823 Log.e(TAG, "Unknown state in doSaveOrCreate: " + mState);
830 * Save the various fields to the existing contact.
832 private void save() {
833 ContentValues values = new ContentValues();
837 // Handle the name and send to voicemail specially
838 final String name = mNameView.getText().toString();
839 if (name != null && TextUtils.isGraphic(name)) {
842 values.put(People.NAME, name);
843 values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
844 mResolver.update(mUri, values, null, null);
847 // Only write the photo if it's changed, since we don't initially load mPhoto
848 if (mPhoto != null) {
849 ByteArrayOutputStream stream = new ByteArrayOutputStream();
850 mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
851 Contacts.People.setPhotoData(mResolver, mUri, stream.toByteArray());
853 Contacts.People.setPhotoData(mResolver, mUri, null);
857 int entryCount = ContactEntryAdapter.countEntries(mSections, false);
858 for (int i = 0; i < entryCount; i++) {
859 EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
860 int kind = entry.kind;
861 data = entry.getData();
862 boolean empty = data == null || !TextUtils.isGraphic(data);
863 if (kind == EditEntry.KIND_CONTACT) {
866 values.put(entry.column, data);
867 mResolver.update(entry.uri, values, null, null);
870 values.put(entry.column, (String) null);
871 mResolver.update(entry.uri, values, null, null);
876 entry.toValues(values);
878 mResolver.update(entry.uri, values, null, null);
880 mResolver.insert(entry.uri, values);
883 } else if (entry.id != 0) {
884 mResolver.delete(entry.uri, null, null);
889 if (numValues == 0) {
890 // The contact is completely empty, delete it
891 mResolver.delete(mUri, null, null);
893 setResult(RESULT_CANCELED);
895 // Add the entry to the my contacts group if it isn't there already
896 People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
897 setResult(RESULT_OK, new Intent().setData(mUri));
899 // Only notify user if we actually changed contact
900 if (mContactChanged || mPhotoChanged) {
901 Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
907 * Takes the entered data and saves it to a new contact.
909 private void create() {
910 ContentValues values = new ContentValues();
914 // Create the contact itself
915 final String name = mNameView.getText().toString();
916 if (name != null && TextUtils.isGraphic(name)) {
919 values.put(People.NAME, name);
920 values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
922 // Add the contact to the My Contacts group
923 Uri contactUri = People.createPersonInMyContactsGroup(mResolver, values);
925 // Add the contact to the group that is being displayed in the contact list
926 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
927 int displayType = prefs.getInt(ContactsListActivity.PREF_DISPLAY_TYPE,
928 ContactsListActivity.DISPLAY_TYPE_UNKNOWN);
929 if (displayType == ContactsListActivity.DISPLAY_TYPE_USER_GROUP) {
930 String displayGroup = prefs.getString(ContactsListActivity.PREF_DISPLAY_INFO,
932 if (!TextUtils.isEmpty(displayGroup)) {
933 People.addToGroup(mResolver, ContentUris.parseId(contactUri), displayGroup);
938 if (mPhoto != null) {
939 ByteArrayOutputStream stream = new ByteArrayOutputStream();
940 mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
941 Contacts.People.setPhotoData(getContentResolver(), contactUri, stream.toByteArray());
944 // Create the contact methods
945 int entryCount = ContactEntryAdapter.countEntries(mSections, false);
946 for (int i = 0; i < entryCount; i++) {
947 EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
948 if (entry.kind != EditEntry.KIND_CONTACT) {
950 if (entry.toValues(values)) {
951 // Only create the entry if there is data
952 entry.uri = mResolver.insert(
953 Uri.withAppendedPath(contactUri, entry.contentDirectory), values);
954 entry.id = ContentUris.parseId(entry.uri);
958 // Update the contact with any straggling data, like notes
959 data = entry.getData();
961 if (data != null && TextUtils.isGraphic(data)) {
962 values.put(entry.column, data);
963 mResolver.update(contactUri, values, null, null);
969 if (numValues == 0) {
970 mResolver.delete(contactUri, null, null);
971 setResult(RESULT_CANCELED);
974 Intent resultIntent = new Intent()
976 .putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
977 setResult(RESULT_OK, resultIntent);
978 Toast.makeText(this, R.string.contactCreatedToast, Toast.LENGTH_SHORT).show();
983 * Build up the entries to display on the screen.
985 * @param extras the extras used to start this activity, may be null
987 private void buildEntriesForEdit(Bundle extras) {
988 Cursor personCursor = mResolver.query(mUri, CONTACT_PROJECTION, null, null, null);
989 if (personCursor == null) {
990 Log.e(TAG, "invalid contact uri: " + mUri);
993 } else if (!personCursor.moveToFirst()) {
994 Log.e(TAG, "invalid contact uri: " + mUri);
996 personCursor.close();
1000 // Clear out the old entries
1001 int numSections = mSections.size();
1002 for (int i = 0; i < numSections; i++) {
1003 mSections.get(i).clear();
1009 mNameView.setText(personCursor.getString(CONTACT_NAME_COLUMN));
1010 mNameView.addTextChangedListener(this);
1013 mPhoto = People.loadContactPhoto(this, mUri, 0, null);
1014 if (mPhoto == null) {
1015 setPhotoPresent(false);
1017 setPhotoPresent(true);
1018 mPhotoImageView.setImageBitmap(mPhoto);
1022 Uri organizationsUri = Uri.withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY);
1023 Cursor organizationsCursor = mResolver.query(organizationsUri, ORGANIZATIONS_PROJECTION,
1026 if (organizationsCursor != null) {
1027 while (organizationsCursor.moveToNext()) {
1028 int type = organizationsCursor.getInt(ORGANIZATIONS_TYPE_COLUMN);
1029 String label = organizationsCursor.getString(ORGANIZATIONS_LABEL_COLUMN);
1030 String company = organizationsCursor.getString(ORGANIZATIONS_COMPANY_COLUMN);
1031 String title = organizationsCursor.getString(ORGANIZATIONS_TITLE_COLUMN);
1032 long id = organizationsCursor.getLong(ORGANIZATIONS_ID_COLUMN);
1033 Uri uri = ContentUris.withAppendedId(Organizations.CONTENT_URI, id);
1035 // Add an organization entry
1036 entry = EditEntry.newOrganizationEntry(this, label, type, company, title, uri, id);
1037 entry.isPrimary = organizationsCursor.getLong(ORGANIZATIONS_ISPRIMARY_COLUMN) != 0;
1038 mOrgEntries.add(entry);
1040 organizationsCursor.close();
1044 if (!personCursor.isNull(CONTACT_NOTES_COLUMN)) {
1045 entry = EditEntry.newNotesEntry(this, personCursor.getString(CONTACT_NOTES_COLUMN),
1047 mNoteEntries.add(entry);
1051 entry = EditEntry.newRingtoneEntry(this,
1052 personCursor.getString(CONTACT_CUSTOM_RINGTONE_COLUMN), mUri);
1053 mOtherEntries.add(entry);
1055 // Send to voicemail
1056 entry = EditEntry.newSendToVoicemailEntry(this,
1057 personCursor.getString(CONTACT_SEND_TO_VOICEMAIL_COLUMN), mUri);
1058 mOtherEntries.add(entry);
1061 mPhoneticNameView.setText(personCursor.getString(CONTACT_PHONETIC_NAME_COLUMN));
1062 mPhoneticNameView.addTextChangedListener(this);
1064 personCursor.close();
1066 // Build up the phone entries
1067 Uri phonesUri = Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY);
1068 Cursor phonesCursor = mResolver.query(phonesUri, PHONES_PROJECTION,
1071 if (phonesCursor != null) {
1072 while (phonesCursor.moveToNext()) {
1073 int type = phonesCursor.getInt(PHONES_TYPE_COLUMN);
1074 String label = phonesCursor.getString(PHONES_LABEL_COLUMN);
1075 String number = phonesCursor.getString(PHONES_NUMBER_COLUMN);
1076 long id = phonesCursor.getLong(PHONES_ID_COLUMN);
1077 boolean isPrimary = phonesCursor.getLong(PHONES_ISPRIMARY_COLUMN) != 0;
1078 Uri uri = ContentUris.withAppendedId(phonesUri, id);
1080 // Add a phone number entry
1081 entry = EditEntry.newPhoneEntry(this, label, type, number, uri, id);
1082 entry.isPrimary = isPrimary;
1083 mPhoneEntries.add(entry);
1085 // Keep track of which primary types have been added
1086 if (type == Phones.TYPE_MOBILE) {
1087 mMobilePhoneAdded = true;
1091 phonesCursor.close();
1094 // Build the contact method entries
1095 Uri methodsUri = Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY);
1096 Cursor methodsCursor = mResolver.query(methodsUri, METHODS_PROJECTION, null, null, null);
1098 if (methodsCursor != null) {
1099 while (methodsCursor.moveToNext()) {
1100 int kind = methodsCursor.getInt(METHODS_KIND_COLUMN);
1101 String label = methodsCursor.getString(METHODS_LABEL_COLUMN);
1102 String data = methodsCursor.getString(METHODS_DATA_COLUMN);
1103 String auxData = methodsCursor.getString(METHODS_AUX_DATA_COLUMN);
1104 int type = methodsCursor.getInt(METHODS_TYPE_COLUMN);
1105 long id = methodsCursor.getLong(METHODS_ID_COLUMN);
1106 boolean isPrimary = methodsCursor.getLong(METHODS_ISPRIMARY_COLUMN) != 0;
1107 Uri uri = ContentUris.withAppendedId(methodsUri, id);
1110 case Contacts.KIND_EMAIL: {
1111 entry = EditEntry.newEmailEntry(this, label, type, data, uri, id);
1112 entry.isPrimary = isPrimary;
1113 mEmailEntries.add(entry);
1116 mPrimaryEmailAdded = true;
1121 case Contacts.KIND_POSTAL: {
1122 entry = EditEntry.newPostalEntry(this, label, type, data, uri, id);
1123 entry.isPrimary = isPrimary;
1124 mPostalEntries.add(entry);
1128 case Contacts.KIND_IM: {
1129 Object protocolObj = ContactMethods.decodeImProtocol(auxData);
1130 if (protocolObj == null) {
1131 // Invalid IM protocol, log it then ignore.
1132 Log.e(TAG, "Couldn't decode IM protocol: " + auxData);
1135 if (protocolObj instanceof Number) {
1136 int protocol = ((Number) protocolObj).intValue();
1137 entry = EditEntry.newImEntry(this,
1138 getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
1141 entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, data,
1144 mImEntries.add(entry);
1151 methodsCursor.close();
1154 // Add values from the extras, if there are any
1155 if (extras != null) {
1156 addFromExtras(extras, phonesUri, methodsUri);
1159 // Add the base types if needed
1160 if (!mMobilePhoneAdded) {
1161 entry = EditEntry.newPhoneEntry(this,
1162 Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY),
1163 DEFAULT_PHONE_TYPE);
1164 mPhoneEntries.add(entry);
1167 if (!mPrimaryEmailAdded) {
1168 entry = EditEntry.newEmailEntry(this,
1169 Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
1170 DEFAULT_EMAIL_TYPE);
1171 entry.isPrimary = true;
1172 mEmailEntries.add(entry);
1175 mContactChanged = false;
1179 * Build the list of EditEntries for full mode insertions.
1181 * @param extras the extras used to start this activity, may be null
1183 private void buildEntriesForInsert(Bundle extras) {
1184 // Clear out the old entries
1185 int numSections = mSections.size();
1186 for (int i = 0; i < numSections; i++) {
1187 mSections.get(i).clear();
1192 // Check the intent extras
1193 if (extras != null) {
1194 addFromExtras(extras, null, null);
1198 mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
1200 // Add the base entries if they're not already present
1201 if (!mMobilePhoneAdded) {
1202 entry = EditEntry.newPhoneEntry(this, null, Phones.TYPE_MOBILE);
1203 entry.isPrimary = true;
1204 mPhoneEntries.add(entry);
1207 if (!mPrimaryEmailAdded) {
1208 entry = EditEntry.newEmailEntry(this, null, DEFAULT_EMAIL_TYPE);
1209 entry.isPrimary = true;
1210 mEmailEntries.add(entry);
1214 entry = EditEntry.newRingtoneEntry(this, null, mUri);
1215 mOtherEntries.add(entry);
1217 // Send to voicemail
1218 entry = EditEntry.newSendToVoicemailEntry(this, "0", mUri);
1219 mOtherEntries.add(entry);
1222 private void addFromExtras(Bundle extras, Uri phonesUri, Uri methodsUri) {
1225 // Read the name from the bundle
1226 CharSequence name = extras.getCharSequence(Insert.NAME);
1227 if (name != null && TextUtils.isGraphic(name)) {
1228 mNameView.setText(name);
1231 // Read the phonetic name from the bundle
1232 CharSequence phoneticName = extras.getCharSequence(Insert.PHONETIC_NAME);
1233 if (!TextUtils.isEmpty(phoneticName)) {
1234 mPhoneticNameView.setText(phoneticName);
1237 // Postal entries from extras
1238 CharSequence postal = extras.getCharSequence(Insert.POSTAL);
1239 int postalType = extras.getInt(Insert.POSTAL_TYPE, INVALID_TYPE);
1240 if (!TextUtils.isEmpty(postal) && postalType == INVALID_TYPE) {
1241 postalType = DEFAULT_POSTAL_TYPE;
1244 if (postalType != INVALID_TYPE) {
1245 entry = EditEntry.newPostalEntry(this, null, postalType, postal.toString(),
1247 entry.isPrimary = extras.getBoolean(Insert.POSTAL_ISPRIMARY);
1248 mPostalEntries.add(entry);
1251 // Email entries from extras
1252 addEmailFromExtras(extras, methodsUri, Insert.EMAIL, Insert.EMAIL_TYPE,
1253 Insert.EMAIL_ISPRIMARY);
1254 addEmailFromExtras(extras, methodsUri, Insert.SECONDARY_EMAIL, Insert.SECONDARY_EMAIL_TYPE,
1256 addEmailFromExtras(extras, methodsUri, Insert.TERTIARY_EMAIL, Insert.TERTIARY_EMAIL_TYPE,
1259 // Phone entries from extras
1260 addPhoneFromExtras(extras, phonesUri, Insert.PHONE, Insert.PHONE_TYPE,
1261 Insert.PHONE_ISPRIMARY);
1262 addPhoneFromExtras(extras, phonesUri, Insert.SECONDARY_PHONE, Insert.SECONDARY_PHONE_TYPE,
1264 addPhoneFromExtras(extras, phonesUri, Insert.TERTIARY_PHONE, Insert.TERTIARY_PHONE_TYPE,
1267 // IM entries from extras
1268 CharSequence imHandle = extras.getCharSequence(Insert.IM_HANDLE);
1269 CharSequence imProtocol = extras.getCharSequence(Insert.IM_PROTOCOL);
1271 if (imHandle != null && imProtocol != null) {
1272 Object protocolObj = ContactMethods.decodeImProtocol(imProtocol.toString());
1273 if (protocolObj instanceof Number) {
1274 int protocol = ((Number) protocolObj).intValue();
1275 entry = EditEntry.newImEntry(this,
1276 getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
1277 imHandle.toString(), null, 0);
1279 entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, imHandle.toString(),
1282 entry.isPrimary = extras.getBoolean(Insert.IM_ISPRIMARY);
1283 mImEntries.add(entry);
1287 private void addEmailFromExtras(Bundle extras, Uri methodsUri, String emailField,
1288 String typeField, String primaryField) {
1289 CharSequence email = extras.getCharSequence(emailField);
1291 // Correctly handle String in typeField as TYPE_CUSTOM
1292 int emailType = INVALID_TYPE;
1293 String customLabel = null;
1294 if(extras.get(typeField) instanceof String) {
1295 emailType = ContactMethods.TYPE_CUSTOM;
1296 customLabel = extras.getString(typeField);
1298 emailType = extras.getInt(typeField, INVALID_TYPE);
1301 if (!TextUtils.isEmpty(email) && emailType == INVALID_TYPE) {
1302 emailType = DEFAULT_EMAIL_TYPE;
1303 mPrimaryEmailAdded = true;
1306 if (emailType != INVALID_TYPE) {
1307 EditEntry entry = EditEntry.newEmailEntry(this, customLabel, emailType, email.toString(),
1309 entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
1310 mEmailEntries.add(entry);
1312 // Keep track of which primary types have been added
1313 if (entry.isPrimary) {
1314 mPrimaryEmailAdded = true;
1319 private void addPhoneFromExtras(Bundle extras, Uri phonesUri, String phoneField,
1320 String typeField, String primaryField) {
1321 CharSequence phoneNumber = extras.getCharSequence(phoneField);
1323 // Correctly handle String in typeField as TYPE_CUSTOM
1324 int phoneType = INVALID_TYPE;
1325 String customLabel = null;
1326 if(extras.get(typeField) instanceof String) {
1327 phoneType = Phones.TYPE_CUSTOM;
1328 customLabel = extras.getString(typeField);
1330 phoneType = extras.getInt(typeField, INVALID_TYPE);
1333 if (!TextUtils.isEmpty(phoneNumber) && phoneType == INVALID_TYPE) {
1334 phoneType = DEFAULT_PHONE_TYPE;
1337 if (phoneType != INVALID_TYPE) {
1338 EditEntry entry = EditEntry.newPhoneEntry(this, customLabel, phoneType,
1339 phoneNumber.toString(), phonesUri, 0);
1340 entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
1341 mPhoneEntries.add(entry);
1343 // Keep track of which primary types have been added
1344 if (phoneType == Phones.TYPE_MOBILE) {
1345 mMobilePhoneAdded = true;
1351 * Removes all existing views, builds new ones for all the entries, and adds them.
1353 private void buildViews() {
1354 // Remove existing views
1355 final LinearLayout layout = mLayout;
1356 layout.removeAllViews();
1358 buildViewsForSection(layout, mPhoneEntries,
1359 R.string.listSeparatorCallNumber_edit, SECTION_PHONES);
1360 buildViewsForSection(layout, mEmailEntries,
1361 R.string.listSeparatorSendEmail_edit, SECTION_EMAIL);
1362 buildViewsForSection(layout, mImEntries,
1363 R.string.listSeparatorSendIm_edit, SECTION_IM);
1364 buildViewsForSection(layout, mPostalEntries,
1365 R.string.listSeparatorMapAddress_edit, SECTION_POSTAL);
1366 buildViewsForSection(layout, mOrgEntries,
1367 R.string.listSeparatorOrganizations, SECTION_ORG);
1368 buildViewsForSection(layout, mNoteEntries,
1369 R.string.label_notes, SECTION_NOTE);
1371 buildOtherViews(layout, mOtherEntries);
1376 * Builds the views for a specific section.
1378 * @param layout the container
1379 * @param section the section to build the views for
1381 private void buildViewsForSection(final LinearLayout layout, ArrayList<EditEntry> section,
1382 int separatorResource, int sectionType) {
1384 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1385 layout.addView(divider);
1387 // Count up undeleted children
1388 int activeChildren = 0;
1389 for (int i = section.size() - 1; i >= 0; i--) {
1390 EditEntry entry = section.get(i);
1391 if (!entry.isDeleted) {
1396 // Build the correct group header based on undeleted children
1398 if (activeChildren == 0) {
1399 header = (ViewGroup) mInflater.inflate(R.layout.edit_separator_alone, layout, false);
1401 header = (ViewGroup) mInflater.inflate(R.layout.edit_separator, layout, false);
1404 // Because we're emulating a ListView, we need to handle focus changes
1405 // with some additional logic.
1406 header.setOnFocusChangeListener(this);
1408 TextView text = (TextView) header.findViewById(R.id.text);
1409 text.setText(getText(separatorResource));
1411 // Force TextView to always default color if we have children. This makes sure
1412 // we don't change color when parent is pressed.
1413 if (activeChildren > 0) {
1414 ColorStateList stateList = text.getTextColors();
1415 text.setTextColor(stateList.getDefaultColor());
1418 View addView = header.findViewById(R.id.separator);
1419 addView.setTag(Integer.valueOf(sectionType));
1420 addView.setOnClickListener(this);
1422 // Build views for the current section
1423 for (EditEntry entry : section) {
1424 entry.activity = this; // this could be null from when the state is restored
1425 if (!entry.isDeleted) {
1426 View view = buildViewForEntry(entry);
1427 header.addView(view);
1431 layout.addView(header);
1434 private void buildOtherViews(final LinearLayout layout, ArrayList<EditEntry> section) {
1435 // Build views for the current section, putting a divider between each one
1436 for (EditEntry entry : section) {
1437 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1438 layout.addView(divider);
1440 entry.activity = this; // this could be null from when the state is restored
1441 View view = buildViewForEntry(entry);
1442 view.setOnClickListener(this);
1443 layout.addView(view);
1446 View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
1447 layout.addView(divider);
1451 * Builds a view to display an EditEntry.
1453 * @param entry the entry to display
1454 * @return a view that will display the given entry
1456 /* package */ View buildViewForEntry(final EditEntry entry) {
1457 // Look for any existing entered text, and save it if found
1458 if (entry.view != null && entry.syncDataWithView) {
1459 String enteredText = ((TextView) entry.view.findViewById(R.id.data))
1460 .getText().toString();
1461 if (!TextUtils.isEmpty(enteredText)) {
1462 entry.data = enteredText;
1467 final ViewGroup parent = mLayout;
1470 // Because we're emulating a ListView, we might need to handle focus changes
1471 // with some additional logic.
1472 if (entry.kind == Contacts.KIND_ORGANIZATION) {
1473 view = mInflater.inflate(R.layout.edit_contact_entry_org, parent, false);
1474 } else if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
1475 view = mInflater.inflate(R.layout.edit_contact_entry_ringtone, parent, false);
1476 view.setOnFocusChangeListener(this);
1477 } else if (isOtherEntry(entry, People.SEND_TO_VOICEMAIL)) {
1478 view = mInflater.inflate(R.layout.edit_contact_entry_voicemail, parent, false);
1479 view.setOnFocusChangeListener(this);
1480 } else if (!entry.isStaticLabel) {
1481 view = mInflater.inflate(R.layout.edit_contact_entry, parent, false);
1483 view = mInflater.inflate(R.layout.edit_contact_entry_static_label, parent, false);
1487 // Set the entry as the tag so we can find it again later given just the view
1491 entry.bindLabel(this);
1494 TextView data = (TextView) view.findViewById(R.id.data);
1495 TextView data2 = (TextView) view.findViewById(R.id.data2);
1497 if (data instanceof Button) {
1498 data.setOnClickListener(this);
1500 if (data.length() == 0) {
1501 if (entry.syncDataWithView) {
1502 // If there is already data entered don't overwrite it
1503 data.setText(entry.data);
1505 fillViewData(entry);
1508 if (data2 != null && data2.length() == 0) {
1509 // If there is already data entered don't overwrite it
1510 data2.setText(entry.data2);
1512 data.setHint(entry.hint);
1513 if (data2 != null) data2.setHint(entry.hint2);
1514 if (entry.lines > 1) {
1515 data.setLines(entry.lines);
1516 data.setMaxLines(entry.maxLines);
1517 if (data2 != null) {
1518 data2.setLines(entry.lines);
1519 data2.setMaxLines(entry.maxLines);
1522 int contentType = entry.contentType;
1523 if (contentType != EditorInfo.TYPE_NULL) {
1524 data.setInputType(contentType);
1525 if (data2 != null) {
1526 data2.setInputType(contentType);
1528 if ((contentType&EditorInfo.TYPE_MASK_CLASS)
1529 == EditorInfo.TYPE_CLASS_PHONE) {
1530 data.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
1531 if (data2 != null) {
1532 data2.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
1537 // Connect listeners up to watch for changed values.
1538 if (data instanceof EditText) {
1539 data.addTextChangedListener(this);
1541 if (data2 instanceof EditText) {
1542 data2.addTextChangedListener(this);
1545 // Hook up the delete button
1546 View delete = view.findViewById(R.id.delete);
1547 if (delete != null) delete.setOnClickListener(this);
1552 private void fillViewData(final EditEntry entry) {
1553 if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
1554 updateRingtoneView(entry);
1555 } else if (isOtherEntry(entry, People.SEND_TO_VOICEMAIL)) {
1556 CheckBox checkBox = (CheckBox) entry.view.findViewById(R.id.checkbox);
1557 boolean sendToVoicemail = false;
1558 if (entry.data != null) {
1559 sendToVoicemail = (Integer.valueOf(entry.data) == 1);
1561 checkBox.setChecked(sendToVoicemail);
1566 * Handles the results from the label change picker.
1568 private final class LabelPickedListener implements DialogInterface.OnClickListener {
1572 public LabelPickedListener(EditEntry entry, String[] labels) {
1577 public void onClick(DialogInterface dialog, int which) {
1578 // TODO: Use a managed dialog
1579 if (mEntry.kind != Contacts.KIND_IM) {
1580 final int type = getTypeFromLabelPosition(mLabels, which);
1581 if (type == ContactMethods.TYPE_CUSTOM) {
1582 createCustomPicker(mEntry, null);
1584 mEntry.setLabel(EditContactActivity.this, type, mLabels[which]);
1585 mContactChanged = true;
1588 mEntry.setLabel(EditContactActivity.this, which, mLabels[which]);
1589 mContactChanged = true;
1595 * A basic structure with the data for a contact entry in the list.
1597 private static final class EditEntry extends ContactEntryAdapter.Entry implements Parcelable {
1598 // These aren't stuffed into the parcel
1599 public EditContactActivity activity;
1602 // These are stuffed into the parcel
1604 public String hint2;
1605 public String column;
1606 public String contentDirectory;
1607 public String data2;
1608 public int contentType;
1611 * If 0 or 1, setSingleLine will be called. If negative, setSingleLine
1612 * will not be called.
1614 public int lines = 1;
1615 public boolean isPrimary;
1616 public boolean isDeleted = false;
1617 public boolean isStaticLabel = false;
1618 public boolean syncDataWithView = true;
1620 private EditEntry() {
1621 // only used by CREATOR
1624 public EditEntry(EditContactActivity activity) {
1625 this.activity = activity;
1628 public EditEntry(EditContactActivity activity, String label,
1629 int type, String data, Uri uri, long id) {
1630 this.activity = activity;
1631 this.isPrimary = false;
1639 public int describeContents() {
1643 public void writeToParcel(Parcel parcel, int flags) {
1644 // Make sure to read data from the input field, if anything is entered
1647 // Write in our own fields.
1648 parcel.writeString(hint);
1649 parcel.writeString(hint2);
1650 parcel.writeString(column);
1651 parcel.writeString(contentDirectory);
1652 parcel.writeString(data2);
1653 parcel.writeInt(contentType);
1654 parcel.writeInt(type);
1655 parcel.writeInt(lines);
1656 parcel.writeInt(isPrimary ? 1 : 0);
1657 parcel.writeInt(isDeleted ? 1 : 0);
1658 parcel.writeInt(isStaticLabel ? 1 : 0);
1659 parcel.writeInt(syncDataWithView ? 1 : 0);
1661 // Write in the fields from Entry
1662 super.writeToParcel(parcel);
1665 public static final Parcelable.Creator<EditEntry> CREATOR =
1666 new Parcelable.Creator<EditEntry>() {
1667 public EditEntry createFromParcel(Parcel in) {
1668 EditEntry entry = new EditEntry();
1670 // Read out our own fields
1671 entry.hint = in.readString();
1672 entry.hint2 = in.readString();
1673 entry.column = in.readString();
1674 entry.contentDirectory = in.readString();
1675 entry.data2 = in.readString();
1676 entry.contentType = in.readInt();
1677 entry.type = in.readInt();
1678 entry.lines = in.readInt();
1679 entry.isPrimary = in.readInt() == 1;
1680 entry.isDeleted = in.readInt() == 1;
1681 entry.isStaticLabel = in.readInt() == 1;
1682 entry.syncDataWithView = in.readInt() == 1;
1684 // Read out the fields from Entry
1685 entry.readFromParcel(in);
1690 public EditEntry[] newArray(int size) {
1691 return new EditEntry[size];
1695 public void setLabel(Context context, int typeIn, String labelIn) {
1703 public void bindLabel(Context context) {
1704 TextView v = (TextView) view.findViewById(R.id.label);
1705 if (isStaticLabel) {
1711 case Contacts.KIND_PHONE: {
1712 v.setText(Phones.getDisplayLabel(context, type, label));
1716 case Contacts.KIND_IM: {
1717 v.setText(getLabelsForKind(activity, kind)[type]);
1721 case Contacts.KIND_ORGANIZATION: {
1722 v.setText(Organizations.getDisplayLabel(activity, type, label));
1727 v.setText(Contacts.ContactMethods.getDisplayLabel(context, kind, type, label));
1728 if (kind == Contacts.KIND_POSTAL) {
1734 v.setOnClickListener(activity);
1738 * Returns the data for the entry
1739 * @return the data for the entry
1741 public String getData() {
1742 if (view != null && syncDataWithView) {
1743 CharSequence text = ((TextView) view.findViewById(R.id.data)).getText();
1745 return text.toString();
1750 return data.toString();
1757 * Dumps the entry into a HashMap suitable for passing to the database.
1759 * @param values the HashMap to fill in.
1760 * @return true if the value should be saved, false otherwise
1762 public boolean toValues(ContentValues values) {
1763 boolean success = false;
1764 String labelString = null;
1765 // Save the type and label
1767 // Read the possibly updated label from the text field
1768 labelString = ((TextView) view.findViewById(R.id.label)).getText().toString();
1771 case Contacts.KIND_PHONE:
1772 if (type != Phones.TYPE_CUSTOM) {
1775 values.put(Phones.LABEL, labelString);
1776 values.put(Phones.TYPE, type);
1779 case Contacts.KIND_EMAIL:
1780 if (type != ContactMethods.TYPE_CUSTOM) {
1783 values.put(ContactMethods.LABEL, labelString);
1784 values.put(ContactMethods.KIND, kind);
1785 values.put(ContactMethods.TYPE, type);
1788 case Contacts.KIND_IM:
1789 values.put(ContactMethods.KIND, kind);
1790 values.put(ContactMethods.TYPE, ContactMethods.TYPE_OTHER);
1791 values.putNull(ContactMethods.LABEL);
1793 values.put(ContactMethods.AUX_DATA,
1794 ContactMethods.encodePredefinedImProtocol(type));
1796 values.put(ContactMethods.AUX_DATA,
1797 ContactMethods.encodeCustomImProtocol(label.toString()));
1801 case Contacts.KIND_POSTAL:
1802 if (type != ContactMethods.TYPE_CUSTOM) {
1805 values.put(ContactMethods.LABEL, labelString);
1806 values.put(ContactMethods.KIND, kind);
1807 values.put(ContactMethods.TYPE, type);
1810 case Contacts.KIND_ORGANIZATION:
1811 if (type != ContactMethods.TYPE_CUSTOM) {
1814 values.put(ContactMethods.LABEL, labelString);
1815 values.put(ContactMethods.TYPE, type);
1818 // Read the possibly updated data from the text field
1819 data2 = ((TextView) view.findViewById(R.id.data2)).getText().toString();
1821 if (!TextUtils.isGraphic(data2)) {
1822 values.putNull(Organizations.TITLE);
1824 values.put(Organizations.TITLE, data2.toString());
1830 Log.w(TAG, "unknown kind " + kind);
1831 values.put(ContactMethods.LABEL, labelString);
1832 values.put(ContactMethods.KIND, kind);
1833 values.put(ContactMethods.TYPE, type);
1837 // Only set the ISPRIMARY flag if part of the incoming data. This is because the
1838 // ContentProvider will try finding a new primary when setting to false, meaning
1839 // it's possible to lose primary altogether as we walk down the list. If this editor
1840 // implements editing of primaries in the future, this will need to be revisited.
1842 values.put(ContactMethods.ISPRIMARY, 1);
1846 if (view != null && syncDataWithView) {
1847 // Read the possibly updated data from the text field
1848 data = ((TextView) view.findViewById(R.id.data)).getText().toString();
1850 if (!TextUtils.isGraphic(data)) {
1851 values.putNull(column);
1854 values.put(column, data.toString());
1860 * Create a new empty organization entry
1862 public static final EditEntry newOrganizationEntry(EditContactActivity activity,
1863 Uri uri, int type) {
1864 return newOrganizationEntry(activity, null, type, null, null, uri, 0);
1868 * Create a new company entry with the given data.
1870 public static final EditEntry newOrganizationEntry(EditContactActivity activity,
1871 String label, int type, String company, String title, Uri uri, long id) {
1872 EditEntry entry = new EditEntry(activity, label, type, company, uri, id);
1873 entry.hint = activity.getString(R.string.ghostData_company);
1874 entry.hint2 = activity.getString(R.string.ghostData_title);
1875 entry.data2 = title;
1876 entry.column = Organizations.COMPANY;
1877 entry.contentDirectory = Organizations.CONTENT_DIRECTORY;
1878 entry.kind = Contacts.KIND_ORGANIZATION;
1879 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
1880 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
1885 * Create a new notes entry with the given data.
1887 public static final EditEntry newNotesEntry(EditContactActivity activity,
1888 String data, Uri uri) {
1889 EditEntry entry = new EditEntry(activity);
1890 entry.label = activity.getString(R.string.label_notes);
1891 entry.hint = activity.getString(R.string.ghostData_notes);
1894 entry.column = People.NOTES;
1895 entry.maxLines = 10;
1898 entry.kind = KIND_CONTACT;
1899 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
1900 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
1901 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
1902 entry.isStaticLabel = true;
1907 * Create a new ringtone entry with the given data.
1909 public static final EditEntry newRingtoneEntry(EditContactActivity activity,
1910 String data, Uri uri) {
1911 EditEntry entry = new EditEntry(activity);
1912 entry.label = activity.getString(R.string.label_ringtone);
1915 entry.column = People.CUSTOM_RINGTONE;
1916 entry.kind = KIND_CONTACT;
1917 entry.isStaticLabel = true;
1918 entry.syncDataWithView = false;
1924 * Create a new send-to-voicemail entry with the given data.
1926 public static final EditEntry newSendToVoicemailEntry(EditContactActivity activity,
1927 String data, Uri uri) {
1928 EditEntry entry = new EditEntry(activity);
1929 entry.label = activity.getString(R.string.actionIncomingCall);
1932 entry.column = People.SEND_TO_VOICEMAIL;
1933 entry.kind = KIND_CONTACT;
1934 entry.isStaticLabel = true;
1935 entry.syncDataWithView = false;
1941 * Create a new empty email entry
1943 public static final EditEntry newPhoneEntry(EditContactActivity activity,
1944 Uri uri, int type) {
1945 return newPhoneEntry(activity, null, type, null, uri, 0);
1949 * Create a new phone entry with the given data.
1951 public static final EditEntry newPhoneEntry(EditContactActivity activity,
1952 String label, int type, String data, Uri uri,
1954 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
1955 entry.hint = activity.getString(R.string.ghostData_phone);
1956 entry.column = People.Phones.NUMBER;
1957 entry.contentDirectory = People.Phones.CONTENT_DIRECTORY;
1958 entry.kind = Contacts.KIND_PHONE;
1959 entry.contentType = EditorInfo.TYPE_CLASS_PHONE;
1964 * Create a new empty email entry
1966 public static final EditEntry newEmailEntry(EditContactActivity activity,
1967 Uri uri, int type) {
1968 return newEmailEntry(activity, null, type, null, uri, 0);
1972 * Create a new email entry with the given data.
1974 public static final EditEntry newEmailEntry(EditContactActivity activity,
1975 String label, int type, String data, Uri uri,
1977 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
1978 entry.hint = activity.getString(R.string.ghostData_email);
1979 entry.column = ContactMethods.DATA;
1980 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
1981 entry.kind = Contacts.KIND_EMAIL;
1982 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
1983 | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
1988 * Create a new empty postal address entry
1990 public static final EditEntry newPostalEntry(EditContactActivity activity,
1991 Uri uri, int type) {
1992 return newPostalEntry(activity, null, type, null, uri, 0);
1996 * Create a new postal address entry with the given data.
1998 * @param label label for the item, from the db not the display label
1999 * @param type the type of postal address
2000 * @param data the starting data for the entry, may be null
2001 * @param uri the uri for the entry if it already exists, may be null
2002 * @param id the id for the entry if it already exists, 0 it it doesn't
2003 * @return the new EditEntry
2005 public static final EditEntry newPostalEntry(EditContactActivity activity,
2006 String label, int type, String data, Uri uri, long id) {
2007 EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
2008 entry.hint = activity.getString(R.string.ghostData_postal);
2009 entry.column = ContactMethods.DATA;
2010 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
2011 entry.kind = Contacts.KIND_POSTAL;
2012 entry.contentType = EditorInfo.TYPE_CLASS_TEXT
2013 | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
2014 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
2015 | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
2022 * Create a new IM address entry
2024 public static final EditEntry newImEntry(EditContactActivity activity,
2025 Uri uri, int type) {
2026 return newImEntry(activity, null, type, null, uri, 0);
2030 * Create a new IM address entry with the given data.
2032 * @param label label for the item, from the db not the display label
2033 * @param protocol the type used
2034 * @param data the starting data for the entry, may be null
2035 * @param uri the uri for the entry if it already exists, may be null
2036 * @param id the id for the entry if it already exists, 0 it it doesn't
2037 * @return the new EditEntry
2039 public static final EditEntry newImEntry(EditContactActivity activity,
2040 String label, int protocol, String data, Uri uri, long id) {
2041 EditEntry entry = new EditEntry(activity, label, protocol, data, uri, id);
2042 entry.hint = activity.getString(R.string.ghostData_im);
2043 entry.column = ContactMethods.DATA;
2044 entry.contentDirectory = People.ContactMethods.CONTENT_DIRECTORY;
2045 entry.kind = Contacts.KIND_IM;
2046 entry.contentType = EditorInfo.TYPE_CLASS_TEXT;
2051 public void afterTextChanged(Editable s) {
2052 // Someone edited a text field, so assume this contact is changed
2053 mContactChanged = true;
2056 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2057 // Do nothing; editing handled by afterTextChanged()
2060 public void onTextChanged(CharSequence s, int start, int before, int count) {
2061 // Do nothing; editing handled by afterTextChanged()
2064 public void onFocusChange(View v, boolean hasFocus) {
2065 // Because we're emulating a ListView, we need to setSelected() for
2066 // views as they are focused.
2067 v.setSelected(hasFocus);