android:title="@string/menu_accounts" />
<item
- android:id="@+id/menu_import"
- android:icon="@drawable/ic_menu_import_contact"
- android:title="@string/importFromSim" />
-
- <item
- android:id="@+id/menu_export"
+ android:id="@+id/menu_import_export"
android:icon="@drawable/ic_menu_export_contact"
- android:title="@string/export_contact_list" />
+ android:title="@string/menu_import_export" />
</menu>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_edit"
+ android:icon="@android:drawable/ic_menu_edit"
+ android:title="@string/menu_editContact"
+ android:alphabeticShortcut="e" />
+
+ <item
+ android:id="@+id/menu_share"
+ android:icon="@android:drawable/ic_menu_share"
+ android:title="@string/menu_share"
+ android:alphabeticShortcut="s" />
+
+ <item
+ android:id="@+id/menu_options"
+ android:icon="@drawable/ic_menu_mark"
+ android:title="@string/menu_contactOptions" />
+
+ <!-- TODO: use new split/join icons -->
+ <item
+ android:id="@+id/menu_split"
+ android:icon="@android:drawable/ic_menu_share"
+ android:title="@string/menu_splitAggregate" />
+
+ <item
+ android:id="@+id/menu_join"
+ android:icon="@android:drawable/ic_menu_add"
+ android:title="@string/menu_joinAggregate" />
+
+ <item
+ android:id="@+id/menu_delete"
+ android:icon="@android:drawable/ic_menu_delete"
+ android:title="@string/menu_deleteContact" />
+
+</menu>
<item type="id" name="dialog_sync_add" />
+ <item type="id" name="dialog_import_export" />
+
</resources>
<!-- Toast displayed when a contact is saved -->
<string name="contactSavedToast">Contact saved.</string>
+ <!-- Toast displayed when saving a contact failed -->
+ <string name="contactSavedErrorToast">Error, unable to save contact changes.</string>
+
<!-- Separator in the contact details list describing that the items below it will place a call when clicked -->
<string name="listSeparatorCallNumber">Dial number</string>
<string name="select_import_type_title">Where would you like to import contacts from?</string>
<!-- Action string for selecting SIM for importing contacts -->
- <string name="import_from_sim">SIM Card</string>
+ <string name="import_from_sim">Import from SIM card</string>
<!-- Action string for selecting SD Card for importing contacts -->
- <string name="import_from_sdcard">SD Card</string>
+ <string name="import_from_sdcard">Import from SD card</string>
+
+ <!-- Action that exports all contacts to SD Card -->
+ <string name="export_to_sdcard">Export to SD card</string>
<!-- "Import one vCard file" -->
<string name="import_one_vcard_string">Import one vCard file</string>
<!-- The menu item to open the list of accounts -->
<string name="menu_accounts">Accounts</string>
+ <!-- The menu item to bulk import or bulk export contacts from SIM card or SD card. -->
+ <string name="menu_import_export">Import/Export</string>
+
+ <!-- Dialog title when selecting the bulk operation to perform from a list. -->
+ <string name="dialog_import_export">Import/Export contacts</string>
+
+ <!-- The menu item to share the currently viewed contact -->
+ <string name="menu_share">Share</string>
+
+ <!-- Dialog title when picking the application to share a contact with. -->
+ <string name="share_via">Share contact via</string>
+
+ <!-- Toast indicating that sharing a contact has failed. -->
+ <string name="share_error">This contact cannot be shared.</string>
+
+
+
<!-- TODO: add comments to each of these strings to prepare for translation -->
<string name="nameLabelsGroup">Name</string>
<string name="nicknameLabelsGroup">Nickname</string>
<string name="type_radio">Radio</string>
<string name="type_assistant">Assistant</string>
-<string name="type_email_1">Email 1</string>
-<string name="type_email_2">Email 2</string>
-<string name="type_email_3">Email 3</string>
-
-<string name="type_im_1">IM 1</string>
-<string name="type_im_2">IM 2</string>
-<string name="type_im_3">IM 3</string>
-
<string name="type_im_aim">AIM</string>
<string name="type_im_msn">Windows Live</string>
<string name="type_im_yahoo">Yahoo</string>
<string name="email_other">Email other</string>
<string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
-<string name="email_1">Email 1</string>
-<string name="email_2">Email 2</string>
-<string name="email_3">Email 3</string>
<string name="email">Email</string>
<string name="chat_jabber">Chat using Jabber</string>
<string name="chat_other">Chat</string>
-<string name="im_1">Chat 1</string>
-<string name="im_2">Chat 2</string>
-<string name="im_3">Chat 3</string>
-<string name="im">Chat</string>
-
<string name="postal_street">Street</string>
<string name="postal_pobox">PO box</string>
<string name="postal_neighborhood">Neighborhood</string>
import com.android.contacts.ui.DisplayGroupsActivity;
import com.android.contacts.ui.FastTrackWindow;
import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
+import com.android.contacts.util.Constants;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Dialog;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.AsyncQueryHandler;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Intents.Insert;
import android.provider.ContactsContract.Intents.UI;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AlphabetIndexer;
+import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.ListView;
/** Run a search query */
static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
/** Run a search query in PICK mode, but that still launches to VIEW */
- // TODO Remove this mode if we decided it is really not needed.
- /*static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;*/
+ static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
/** Show join suggestions followed by an A-Z list */
static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
static final String NAME_COLUMN = Contacts.DISPLAY_NAME;
//static final String SORT_STRING = People.SORT_STRING;
- static final String[] CONTACTS_PROJECTION = new String[] {
- Contacts._ID, // 0
- Contacts.DISPLAY_NAME, // 1
- Contacts.STARRED, //2
- };
-
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
Contacts._ID, // 0
Contacts.DISPLAY_NAME, // 1
Presence.PRESENCE_STATUS, //4
Contacts.PHOTO_ID, //5
Contacts.HAS_PHONE_NUMBER, //6
+ Contacts.LOOKUP_KEY, //7
};
static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
People._ID, // 0
PeopleColumns.TIMES_CONTACTED, //3
People.PRESENCE_STATUS, //4
};
- static final int ID_COLUMN_INDEX = 0;
+ static final int SUMMARY_ID_COLUMN_INDEX = 0;
static final int SUMMARY_NAME_COLUMN_INDEX = 1;
static final int SUMMARY_STARRED_COLUMN_INDEX = 2;
static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 3;
static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 4;
static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5;
static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 6;
+ static final int SUMMARY_LOOKUP_KEY = 7;
static final String[] PHONES_PROJECTION = new String[] {
Data._ID, //0
// private boolean mDisplayAll;
private boolean mDisplayOnlyPhones;
- /**
- * Cursor row index that holds reference back to {@link People#_ID}, such as
- * {@link ContactMethods#PERSON_ID}. Used when responding to a
- * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
- */
- private int mQueryPersonIdIndex;
-
private Uri mGroupUri;
private long mQueryAggregateId;
private boolean mListHasFocus;
private String mShortcutAction;
- private boolean mDefaultMode = false;
-
- private boolean mCreateShortcut;
/**
* Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
private Handler mHandler = new Handler();
- private class ImportTypeSelectedListener implements DialogInterface.OnClickListener {
- public static final int IMPORT_FROM_SIM = 0;
- public static final int IMPORT_FROM_SDCARD = 1;
-
- private int mIndex;
-
- public ImportTypeSelectedListener() {
- mIndex = IMPORT_FROM_SIM;
- }
-
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- if (mIndex == IMPORT_FROM_SIM) {
- doImportFromSim();
- } else {
- doImportFromSDCard();
- }
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-
- } else {
- mIndex = which;
- }
- }
- }
-
private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
mShortcutAction = Intent.ACTION_VIEW;
setTitle(R.string.shortcutActivityTitle);
}
- mCreateShortcut = true;
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
final String type = intent.resolveType(this);
if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
}
// See if search request has extras to specify query
- /*if (intent.hasExtra(Insert.EMAIL)) {
+ if (intent.hasExtra(Insert.EMAIL)) {
mMode = MODE_QUERY_PICK_TO_VIEW;
mQueryMode = QUERY_MODE_MAILTO;
mQueryData = intent.getStringExtra(Insert.EMAIL);
// Otherwise handle the more normal search case
mMode = MODE_QUERY;
}
- */
mMode = MODE_QUERY;
// Since this is the filter activity it receives all intents
public boolean onPrepareOptionsMenu(Menu menu) {
final boolean defaultMode = (mMode == MODE_DEFAULT);
menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
-
- final boolean allowExport = getResources().getBoolean(R.bool.config_allow_export_to_sdcard);
- menu.findItem(R.id.menu_export).setVisible(allowExport);
-
return true;
}
startActivity(intent);
return true;
}
- case R.id.menu_import: {
- if (getResources().getBoolean(R.bool.config_allow_import_from_sdcard)) {
- ImportTypeSelectedListener listener =
- new ImportTypeSelectedListener();
- AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this)
- .setTitle(R.string.select_import_type_title)
- .setPositiveButton(android.R.string.ok, listener)
- .setNegativeButton(android.R.string.cancel, null);
- dialogBuilder.setSingleChoiceItems(new String[] {
- getString(R.string.import_from_sim),
- getString(R.string.import_from_sdcard)},
- ImportTypeSelectedListener.IMPORT_FROM_SIM, listener);
- dialogBuilder.show();
- } else {
- doImportFromSim();
- }
- return true;
- }
- case R.id.menu_export: {
- handleExportContacts();
+ case R.id.menu_import_export: {
+ showDialog(R.id.dialog_import_export);
return true;
}
case R.id.menu_accounts: {
return false;
}
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case R.id.dialog_import_export: {
+ return createImportExportDialog();
+ }
+ }
+ return super.onCreateDialog(id);
+ }
+
+ /**
+ * Create a {@link Dialog} that allows the user to pick from a bulk import
+ * or bulk export task across all contacts.
+ */
+ private Dialog createImportExportDialog() {
+ // Wrap our context to inflate list items using correct theme
+ final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
+ final Resources res = dialogContext.getResources();
+ final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ // Adapter that shows a list of string resources
+ final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this,
+ android.R.layout.simple_list_item_1) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
+ parent, false);
+ }
+
+ final int resId = this.getItem(position);
+ ((TextView)convertView).setText(resId);
+ return convertView;
+ }
+ };
+
+ if (TelephonyManager.getDefault().hasIccCard()) {
+ adapter.add(R.string.import_from_sim);
+ }
+ if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) {
+ adapter.add(R.string.import_from_sdcard);
+ }
+ if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
+ adapter.add(R.string.export_to_sdcard);
+ }
+
+ final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+
+ final int resId = adapter.getItem(which);
+ switch (resId) {
+ case R.string.import_from_sim: {
+ doImportFromSim();
+ break;
+ }
+ case R.string.import_from_sdcard: {
+ doImportFromSdCard();
+ break;
+ }
+ case R.string.export_to_sdcard: {
+ doExportToSdCard();
+ break;
+ }
+ }
+ }
+ };
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.dialog_import_export);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setSingleChoiceItems(adapter, -1, clickListener);
+ return builder.create();
+ }
+
private void doImportFromSim() {
Intent importIntent = new Intent(Intent.ACTION_VIEW);
importIntent.setType("vnd.android.cursor.item/sim-contact");
startActivity(importIntent);
}
- private void doImportFromSDCard() {
+ private void doImportFromSdCard() {
Intent intent = new Intent(this, ImportVCardActivity.class);
startActivity(intent);
}
- private void handleExportContacts() {
+ private void doExportToSdCard() {
VCardExporter exporter = new VCardExporter(ContactsListActivity.this, mHandler);
exporter.startExportVCardToSdCard();
}
// Toggle the star
ContentValues values = new ContentValues(1);
values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
- Uri aggUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
- cursor.getInt(ID_COLUMN_INDEX));
- getContentResolver().update(aggUri, values, null, null);
+ final Uri selectedUri = this.getContactUri(info.position);
+ getContentResolver().update(selectedUri, values, null, null);
return true;
}
- /* case MENU_ITEM_DELETE: {
- // Get confirmation
- Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
- cursor.getLong(ID_COLUMN_INDEX));
- //TODO make this dialog persist across screen rotations
- new AlertDialog.Builder(ContactsListActivity.this)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.deleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
- .show();
+ case MENU_ITEM_DELETE: {
+ final Uri selectedUri = getContactUri(info.position);
+ doContactDelete(selectedUri);
return true;
- } */
+ }
}
return super.onContextItemSelected(item);
break;
}
case KeyEvent.KEYCODE_DEL: {
- Object o = getListView().getSelectedItem();
- if (o != null) {
- Cursor cursor = (Cursor) o;
- Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
- cursor.getLong(ID_COLUMN_INDEX));
- //TODO make this dialog persist across screen rotations
- new AlertDialog.Builder(ContactsListActivity.this)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.deleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
- .setCancelable(false)
- .show();
+ final int position = getListView().getSelectedItemPosition();
+ if (position != ListView.INVALID_POSITION) {
+ final Uri selectedUri = getContactUri(position);
+ doContactDelete(selectedUri);
return true;
}
break;
return super.onKeyDown(keyCode, event);
}
+ /**
+ * Prompt the user before deleting the given {@link Contacts} entry.
+ */
+ protected void doContactDelete(Uri contactUri) {
+ new AlertDialog.Builder(ContactsListActivity.this)
+ .setTitle(R.string.deleteConfirmation_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.deleteConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, new DeleteClickListener(contactUri))
+ .show();
+ }
+
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// Hide soft keyboard, if visible
intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
} else {
// Edit
- intent = new Intent(Intent.ACTION_EDIT,
- ContentUris.withAppendedId(Contacts.CONTENT_URI, id));
+ final Uri uri = getSelectedUri(position);
+ intent = new Intent(Intent.ACTION_EDIT, uri);
}
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
final Bundle extras = getIntent().getExtras();
startActivity(intent);
finish();
} else if (id != -1) {
- Uri uri = getPickerResultUri(id);
+ final Uri uri = getSelectedUri(position);
if ((mMode & MODE_MASK_PICKER) == 0) {
- Intent intent = new Intent(Intent.ACTION_VIEW,
- ContentUris.withAppendedId(Contacts.CONTENT_URI, id));
+ final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
} else if (mMode == MODE_JOIN_CONTACT) {
returnPickerResult(null, null, uri, id);
- }
-
- /*else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
+ } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
// Started with query that should launch to view contact
- Cursor c = (Cursor) mAdapter.getItem(position);
- long personId = c.getLong(mQueryPersonIdIndex);
- Intent intent = new Intent(Intent.ACTION_VIEW,
- ContentUris.withAppendedId(People.CONTENT_URI, personId));
+ final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
finish();
- }*/ else if (mMode == MODE_PICK_CONTACT
+ } else if (mMode == MODE_PICK_CONTACT
|| mMode == MODE_PICK_OR_CREATE_CONTACT
|| mMode == MODE_LEGACY_PICK_PERSON
|| mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
}
} else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
&& position == 0) {
- // Hook this up to new edit contact activity (bug 2092559)
+ // TODO: Hook this up to new edit contact activity (bug 2092559)
/*Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);*/
} else {
}
}
+ /**
+ * @param uri In most cases, this should be a lookup {@link Uri}, possibly
+ * generated through {@link Contacts#getLookupUri(long, String)}.
+ */
private void returnPickerResult(Cursor c, String name, Uri uri, long id) {
final Intent intent = new Intent();
Intent shortcutIntent;
if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
// This is a simple shortcut to view a contact.
- Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
- shortcutIntent = new Intent(mShortcutAction, lookupUri);
+ shortcutIntent = new Intent(mShortcutAction, uri);
final Bitmap icon = loadContactPhoto(id, null);
if (icon != null) {
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
String scheme;
int resid;
if (Intent.ACTION_CALL.equals(mShortcutAction)) {
- scheme = "tel";
+ scheme = Constants.SCHEME_TEL;
resid = R.drawable.badge_action_call;
} else {
- scheme = "smsto";
+ scheme = Constants.SCHEME_SMSTO;
resid = R.drawable.badge_action_sms;
}
case MODE_LEGACY_PICK_POSTAL: {
return ContactMethods.CONTENT_URI;
}
- default: {
- return null;
+ case MODE_QUERY_PICK_TO_VIEW: {
+ if (mQueryMode == QUERY_MODE_MAILTO) {
+ return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(mQueryData));
+ } else if (mQueryMode == QUERY_MODE_TEL) {
+ return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData));
+ }
}
}
+ return null;
}
- Uri getPickerResultUri(long id) {
+ /**
+ * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+ * {@link ListView} position, using {@link #mAdapter}.
+ */
+ private Uri getContactUri(int position) {
+ if (position == ListView.INVALID_POSITION) {
+ throw new IllegalArgumentException("Position not in list bounds");
+ }
+
+ final Cursor cursor = (Cursor)mAdapter.getItem(position);
switch(mMode) {
- case MODE_PICK_CONTACT:
- case MODE_PICK_OR_CREATE_CONTACT:
- case MODE_JOIN_CONTACT: {
- return ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
+ case MODE_LEGACY_PICK_PERSON:
+ case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
+ final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+ return ContentUris.withAppendedId(People.CONTENT_URI, personId);
+ }
+
+ default: {
+ // Build and return soft, lookup reference
+ final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+ final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
+ return Contacts.getLookupUri(contactId, lookupKey);
}
+ }
+ }
+
+ /**
+ * Build the {@link Uri} for the given {@link ListView} position, which can
+ * be used as result when in {@link #MODE_MASK_PICKER} mode.
+ */
+ private Uri getSelectedUri(int position) {
+ if (position == ListView.INVALID_POSITION) {
+ throw new IllegalArgumentException("Position not in list bounds");
+ }
+
+ final long id = mAdapter.getItemId(position);
+ switch(mMode) {
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return ContentUris.withAppendedId(People.CONTENT_URI, id);
return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
}
default: {
- return null;
+ return getContactUri(position);
}
}
}
String[] getProjectionForQuery() {
switch(mMode) {
- case MODE_JOIN_CONTACT: {
- return CONTACTS_PROJECTION;
- }
+ case MODE_JOIN_CONTACT:
case MODE_STREQUENT:
case MODE_FREQUENT:
case MODE_STARRED:
case MODE_LEGACY_PICK_POSTAL: {
return LEGACY_POSTALS_PROJECTION;
}
+ case MODE_QUERY_PICK_TO_VIEW: {
+ if (mQueryMode == QUERY_MODE_MAILTO) {
+ return CONTACTS_SUMMARY_PROJECTION;
+ } else if (mQueryMode == QUERY_MODE_TEL) {
+ return PHONES_PROJECTION;
+ }
+ break;
+ }
}
// Default to normal aggregate projection
break;
}
- /*
case MODE_QUERY_PICK_TO_VIEW: {
- if (mQueryMode == QUERY_MODE_MAILTO) {
- // Find all contacts with the given search string as E-mail.
- Uri uri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_EMAIL_URI,
- Uri.encode(mQueryData));
- mQueryHandler.startQuery(QUERY_TOKEN, null,
- uri, SIMPLE_CONTACTS_PROJECTION, null, null,
- getSortOrder(CONTACTS_PROJECTION));
-
- } else if (mQueryMode == QUERY_MODE_TEL) {
- mQueryAggIdIndex = PHONES_PERSON_ID_INDEX;
- mQueryHandler.startQuery(QUERY_TOKEN, null,
- Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData),
- PHONES_PROJECTION, null, null,
- getSortOrder(PHONES_PROJECTION));
- }
+ mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
+ getSortOrder(projection));
break;
}
- */
case MODE_STARRED:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
return false;
}
- String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(), cursor.
- getLong(ID_COLUMN_INDEX));
+ // TODO: transition to use lookup instead of strong id
+ final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+ final String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(),
+ contactId);
if (phone == null) {
signalError();
return false;
}
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts("tel", phone, null));
+ Uri.fromParts(Constants.SCHEME_TEL, phone, null));
startActivity(intent);
return true;
}
} else {
activity.mAdapter.setSuggestionsCursor(null);
}
- startQuery(QUERY_TOKEN, null, Contacts.CONTENT_URI, CONTACTS_PROJECTION,
+ startQuery(QUERY_TOKEN, null, Contacts.CONTENT_URI, CONTACTS_SUMMARY_PROJECTION,
Contacts._ID + " != " + mAggregateId, null,
- getSortOrder(CONTACTS_PROJECTION));
+ getSortOrder(CONTACTS_SUMMARY_PROJECTION));
} else {
cursor.close();
import com.android.contacts.model.ContactsSource;
import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
import java.io.ByteArrayInputStream;
int colType;
int colLabel;
- // TODO: move the SMS mime-type to a central location
if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
- || FastTrackWindow.MIME_SMS_ADDRESS.equals(mimeType)) {
+ || Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
// Reset to phone mimetype so we generate a label for SMS case
mimeType = Phone.CONTENT_ITEM_TYPE;
colType = cursor.getColumnIndex(Phone.TYPE);
String phone = null;
try {
Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
- Uri dataUri = Uri.withAppendedPath(baseUri, "data");
+ Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
c = cr.query(dataUri,
new String[] {Phone.NUMBER},
package com.android.contacts;
import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
import android.app.Activity;
RawContacts.CONTACT_ID,
};
- static final String SCHEME_MAILTO = "mailto";
- static final String SCHEME_TEL = "tel";
-
static final int AGGREGATE_ID_INDEX = 0;
static final int QUERY_TOKEN = 42;
mCreateForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
// Handle specific query request
- if (SCHEME_MAILTO.equals(scheme)) {
+ if (Constants.SCHEME_MAILTO.equals(scheme)) {
mCreateExtras.putString(Intents.Insert.EMAIL, ssp);
Uri uri = Uri.withAppendedPath(Email.CONTENT_FILTER_EMAIL_URI, Uri.encode(ssp));
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, CONTACTS_PROJECTION, null, null, null);
- } else if (SCHEME_TEL.equals(scheme)) {
+ } else if (Constants.SCHEME_TEL.equals(scheme)) {
mCreateExtras.putString(Intents.Insert.PHONE, ssp);
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, ssp);
package com.android.contacts;
-import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.Constants;
+import android.accounts.Account;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
/**
- * This class contains utility functions for determining the precedence of different types
- * associated with contact data items.
+ * This class contains utility functions for determining the precedence of
+ * different types associated with contact data items.
+ *
+ * @deprecated use {@link EntityModifier#getTypePrecedence} instead, since this
+ * list isn't {@link Account} based.
*/
+@Deprecated
public final class TypePrecedence {
/* This utility class has cannot be instantiated.*/
* @param type The integer type as defined in {@Link ContactsContract#CommonDataKinds}.
* @return The integer precedence, where 1 is the highest.
*/
+ @Deprecated
public static int getTypePrecedence(String mimetype, int type) {
int[] typePrecedence = getTypePrecedenceList(mimetype);
if (typePrecedence == null) {
return typePrecedence.length;
}
+ @Deprecated
private static int[] getTypePrecedenceList(String mimetype) {
if (mimetype.equals(Phone.CONTENT_ITEM_TYPE)) {
return TYPE_PRECEDENCE_PHONES;
- } else if (mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+ } else if (mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
return TYPE_PRECEDENCE_PHONES;
} else if (mimetype.equals(Email.CONTENT_ITEM_TYPE)) {
return TYPE_PRECEDENCE_EMAIL;
import com.android.contacts.ScrollingTabWidget.OnTabSelectionChangedListener;
import com.android.contacts.SplitAggregateView.OnContactSelectedListener;
import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityModifier;
import com.android.contacts.model.Sources;
import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.HardCodedSources.SimpleInflater;
import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
import com.android.internal.telephony.ITelephony;
import com.android.internal.widget.ContactHeaderWidget;
import android.content.Intent;
import android.content.DialogInterface.OnClickListener;
import android.content.Entity.NamedContentValues;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.Cursor;
-import android.database.DatabaseUtils;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
+import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import java.util.ArrayList;
-import java.util.Iterator;
/**
* Displays the details of a specific contact.
AdapterView.OnItemClickListener, NotifyingAsyncQueryHandler.AsyncQueryListener,
OnTabSelectionChangedListener {
private static final String TAG = "ViewContact";
- private static final String SHOW_BARCODE_INTENT = "com.google.zxing.client.android.ENCODE";
public static final String RAW_CONTACT_ID_EXTRA = "rawContactIdExtra";
private static final int REQUEST_JOIN_CONTACT = 1;
private static final int REQUEST_EDIT_CONTACT = 2;
- public static final int MENU_ITEM_EDIT = 1;
- public static final int MENU_ITEM_DELETE = 2;
public static final int MENU_ITEM_MAKE_DEFAULT = 3;
- public static final int MENU_ITEM_SHOW_BARCODE = 4;
- public static final int MENU_ITEM_SPLIT_AGGREGATE = 5;
- public static final int MENU_ITEM_JOIN_AGGREGATE = 6;
- public static final int MENU_ITEM_OPTIONS = 7;
protected Uri mLookupUri;
private Uri mUri;
// TAB CODE //
/**
- * Adds a tab for each {@link RawContact} associated with this contact.
+ * Adds a tab for each {@link RawContacts} associated with this contact.
* Override this method if you want to additional tabs and/or different
* tabs for your activity.
- *
- * @param entities An {@link ArrayList} of {@link Entity}s of all the RawContacts
- * associated with the contact being displayed.
*/
protected void bindTabs() {
if (mEntities.size() > 1) {
/**
* Add a tab to be displayed in the {@link ScrollingTabWidget}.
*
- * @param contactId The contact id associated with the tab.
+ * @param rawContactId The contact id associated with the tab.
* @param view A view to use as the tab indicator.
*/
protected void addTab(long rawContactId, View view) {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
- .setIcon(android.R.drawable.ic_menu_delete);
- menu.add(0, MENU_ITEM_SPLIT_AGGREGATE, 0, R.string.menu_splitAggregate)
- .setIcon(android.R.drawable.ic_menu_share);
- menu.add(0, MENU_ITEM_JOIN_AGGREGATE, 0, R.string.menu_joinAggregate)
- .setIcon(android.R.drawable.ic_menu_add);
- menu.add(0, MENU_ITEM_OPTIONS, 0, R.string.menu_contactOptions)
- .setIcon(R.drawable.ic_menu_mark);
+ super.onCreateOptionsMenu(menu);
+
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.view, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
- // Perform this check each time the menu is about to be shown, because the Barcode Scanner
- // could be installed or uninstalled at any time.
- if (isBarcodeScannerInstalled()) {
- if (menu.findItem(MENU_ITEM_SHOW_BARCODE) == null) {
- menu.add(0, MENU_ITEM_SHOW_BARCODE, 0, R.string.menu_showBarcode)
- .setIcon(R.drawable.ic_menu_show_barcode);
- }
- } else {
- menu.removeItem(MENU_ITEM_SHOW_BARCODE);
- }
- // Only show the edit option if we have a selected tab.
- if (mSelectedRawContactId != null) {
- if (menu.findItem(MENU_ITEM_EDIT) == null) {
- menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
- .setIcon(android.R.drawable.ic_menu_edit)
- .setAlphabeticShortcut('e');
- }
- } else {
- menu.removeItem(MENU_ITEM_EDIT);
- }
+ // Only allow edit if we have a selected tab
+ final boolean contactSelected = (mSelectedRawContactId != null);
+ menu.findItem(R.id.menu_edit).setEnabled(contactSelected);
- boolean isAggregate = mRawContactIds.size() > 1;
- menu.findItem(MENU_ITEM_SPLIT_AGGREGATE).setEnabled(isAggregate);
- return true;
- }
+ // Only allow split when more than one contact
+ final boolean isAggregate = (mRawContactIds.size() > 1);
+ menu.findItem(R.id.menu_split).setEnabled(isAggregate);
- private boolean isBarcodeScannerInstalled() {
- final Intent intent = new Intent(SHOW_BARCODE_INTENT);
- ResolveInfo ri = getPackageManager().resolveActivity(intent,
- PackageManager.MATCH_DEFAULT_ONLY);
- return ri != null;
+ return true;
}
@Override
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
- case MENU_ITEM_EDIT: {
+ case R.id.menu_edit: {
Long rawContactIdToEdit = mSelectedRawContactId;
if (rawContactIdToEdit == null) {
// This shouldn't be possible. We only show the edit option if
REQUEST_EDIT_CONTACT);
break;
}
- case MENU_ITEM_DELETE: {
+ case R.id.menu_delete: {
// Get confirmation
showDialog(DIALOG_CONFIRM_DELETE);
return true;
}
-
- case MENU_ITEM_SPLIT_AGGREGATE: {
+ case R.id.menu_split: {
if (mRawContactIds.size() == 2) {
splitContact(mRawContactIds.get(1));
} else {
}
return true;
}
-
- case MENU_ITEM_JOIN_AGGREGATE: {
+ case R.id.menu_join: {
showJoinAggregateActivity();
return true;
}
-
- case MENU_ITEM_OPTIONS: {
+ case R.id.menu_options: {
showOptionsActivity();
return true;
}
+ case R.id.menu_share: {
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra(Intent.EXTRA_STREAM, mLookupUri);
- // TODO(emillar) Bring this back.
- /*case MENU_ITEM_SHOW_BARCODE:
- if (mCursor.moveToFirst()) {
- Intent intent = new Intent(SHOW_BARCODE_INTENT);
- intent.putExtra("ENCODE_TYPE", "CONTACT_TYPE");
- Bundle bundle = new Bundle();
- String name = mCursor.getString(AGGREGATE_DISPLAY_NAME_COLUMN);
- if (!TextUtils.isEmpty(name)) {
- // Correctly handle when section headers are hidden
- int sepAdjust = SHOW_SEPARATORS ? 1 : 0;
-
- bundle.putString(Contacts.Intents.Insert.NAME, name);
- // The 0th ViewEntry in each ArrayList below is a separator item
- int entriesToAdd = Math.min(mPhoneEntries.size() - sepAdjust, PHONE_KEYS.length);
- for (int x = 0; x < entriesToAdd; x++) {
- ViewEntry entry = mPhoneEntries.get(x + sepAdjust);
- bundle.putString(PHONE_KEYS[x], entry.data);
- }
- entriesToAdd = Math.min(mEmailEntries.size() - sepAdjust, EMAIL_KEYS.length);
- for (int x = 0; x < entriesToAdd; x++) {
- ViewEntry entry = mEmailEntries.get(x + sepAdjust);
- bundle.putString(EMAIL_KEYS[x], entry.data);
- }
- if (mPostalEntries.size() >= 1 + sepAdjust) {
- ViewEntry entry = mPostalEntries.get(sepAdjust);
- bundle.putString(Contacts.Intents.Insert.POSTAL, entry.data);
- }
- intent.putExtra("ENCODE_DATA", bundle);
- try {
- startActivity(intent);
- } catch (ActivityNotFoundException e) {
- // The check in onPrepareOptionsMenu() should make this impossible, but
- // for safety I'm catching the exception rather than crashing. Ideally
- // I'd call Menu.removeItem() here too, but I don't see a way to get
- // the options menu.
- Log.e(TAG, "Show barcode menu item was clicked but Barcode Scanner " +
- "was not installed.");
- }
- return true;
- }
+ // Launch chooser to share contact via
+ final CharSequence chooseTitle = getText(R.string.share_via);
+ final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+ try {
+ startActivity(chooseIntent);
+ } catch (ActivityNotFoundException ex) {
+ Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
}
- break; */
+ return true;
+ }
}
return super.onOptionsItemSelected(item);
}
entry.mimetype = mimetype;
entry.label = buildActionString(kind, entryValues, true);
entry.data = buildDataString(kind, entryValues);
- if (kind.typeColumn != null) {
+ if (kind.typeColumn != null && entryValues.containsKey(kind.typeColumn)) {
entry.type = entryValues.getAsInteger(kind.typeColumn);
}
if (kind.iconRes > 0) {
String host = null;
if (TextUtils.isEmpty(entry.label)) {
- entry.label = getString(R.string.im).toLowerCase();
+ entry.label = getString(R.string.chat_other).toLowerCase();
}
if (protocolObj instanceof Number) {
TextView data = views.data;
if (data != null) {
if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
- || entry.mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+ || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
data.setText(PhoneNumberUtils.formatNumber(entry.data));
} else {
data.setText(entry.data);
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.BaseColumns;
-import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.view.View;
// Always take after values from new state
this.mValues.mAfter = remote.mValues.mAfter;
+ // TODO: log before/after versions to track re-parenting
+
// Find matching local entry for each remote values, or create
for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
for (ValuesDelta remoteEntry : mimeEntries) {
return mValues;
}
+ public boolean isContactInsert() {
+ return mValues.isInsert();
+ }
+
/**
* Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
* which may return null when no entry exists.
}
/**
+ * Build a list of {@link ContentProviderOperation} that will assert any
+ * "before" state hasn't changed. This is maintained separately so that all
+ * asserts can take place before any updates occur.
+ */
+ public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
+ final boolean isContactInsert = mValues.isInsert();
+ if (!isContactInsert) {
+ // Assert version is consistent while persisting changes
+ final Long beforeId = mValues.getId();
+ final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+
+ final ContentProviderOperation.Builder builder = ContentProviderOperation
+ .newAssertQuery(RawContacts.CONTENT_URI);
+ builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+ builder.withValue(RawContacts.VERSION, beforeVersion);
+ buildInto.add(builder.build());
+ }
+ }
+
+ /**
* Build a list of {@link ContentProviderOperation} that will transform the
* current "before" {@link Entity} state into the modified state which this
* {@link EntityDelta} represents.
*/
- public ArrayList<ContentProviderOperation> buildDiff() {
- final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
+ final int firstIndex = buildInto.size();
final boolean isContactInsert = mValues.isInsert();
final boolean isContactDelete = mValues.isDelete();
final boolean isContactUpdate = !isContactInsert && !isContactDelete;
final Long beforeId = mValues.getId();
- final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
Builder builder;
// Build possible operation at Contact level
builder = mValues.buildDiff(RawContacts.CONTENT_URI);
- possibleAdd(diff, builder);
+ possibleAdd(buildInto, builder);
// Build operations for all children
for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
if (child.isInsert()) {
if (isContactInsert) {
// Parent is brand new insert, so back-reference _id
- builder.withValueBackReference(Data.RAW_CONTACT_ID, 0);
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
} else {
// Inserting under existing, so fill with known _id
builder.withValue(Data.RAW_CONTACT_ID, beforeId);
// Child must be insert when Contact insert
throw new IllegalArgumentException("When parent insert, child must be also");
}
- possibleAdd(diff, builder);
+ possibleAdd(buildInto, builder);
}
}
- // Create exception when insert requested aggregate membership
- final Long contactId = mValues.getAsLong(RawContacts.CONTACT_ID);
- if (isContactInsert && contactId != null) {
- builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
- builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_IN);
- builder.withValue(AggregationExceptions.CONTACT_ID, contactId);
- builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID, 0);
- possibleAdd(diff, builder);
- }
-
- final boolean hasOperations = diff.size() > 0;
-
- if (hasOperations && isContactUpdate) {
+ final boolean addedOperations = buildInto.size() > firstIndex;
+ if (addedOperations && isContactUpdate) {
// Suspend aggregation while persisting updates
builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
- diff.add(0, builder.build());
+ buildInto.add(firstIndex, builder.build());
// Restore aggregation as last operation
builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
- diff.add(builder.build());
+ buildInto.add(builder.build());
}
-
- if (hasOperations && (isContactUpdate || isContactDelete)) {
- // Assert version is consistent while persisting changes
- builder = ContentProviderOperation.newAssertQuery(RawContacts.CONTENT_URI);
- builder.withSelection(RawContacts._ID + "=" + beforeId, null);
- builder.withValue(RawContacts.VERSION, beforeVersion);
- // Sneak version check at beginning of list--we only depend on
- // back-references during insert cases.
- diff.add(0, builder.build());
- }
-
- return diff;
}
/**
*/
public static EditType getCurrentType(ContentValues entry, DataKind kind) {
if (kind.typeColumn == null) return null;
- final int rawValue = entry.getAsInteger(kind.typeColumn);
+ final Integer rawValue = entry.getAsInteger(kind.typeColumn);
+ if (rawValue == null) return null;
return getType(kind, rawValue);
}
public static EditType getCurrentType(Cursor cursor, DataKind kind) {
if (kind.typeColumn == null) return null;
final int index = cursor.getColumnIndex(kind.typeColumn);
+ if (index == -1) return null;
final int rawValue = cursor.getInt(index);
return getType(kind, rawValue);
}
}
/**
+ * Return the precedence for the the given {@link EditType#rawValue}, where
+ * lower numbers are higher precedence.
+ */
+ public static int getTypePrecedence(DataKind kind, int rawValue) {
+ for (int i = 0; i < kind.typeList.size(); i++) {
+ final EditType type = kind.typeList.get(i);
+ if (type.rawValue == rawValue) {
+ return i;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+
+ /**
* Find the best {@link EditType} for a potential insert. The "best" is the
* first primary type that doesn't already exist. When all valid types
* exist, we pick the last valid option.
--- /dev/null
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.model;
+
+import com.google.android.collect.Lists;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.ContentProviderOperation.Builder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+import java.util.ArrayList;
+
+/**
+ * Container for multiple {@link EntityDelta} objects, usually when editing
+ * together as an entire aggregate. Provides convenience methods for parceling
+ * and applying another {@link EntitySet} over it.
+ */
+public class EntitySet extends ArrayList<EntityDelta> implements Parcelable {
+ private EntitySet() {
+ }
+
+ /**
+ * Create an {@link EntitySet} that contains the given {@link EntityDelta},
+ * usually when inserting a new {@link Contacts} entry.
+ */
+ public static EntitySet fromSingle(EntityDelta delta) {
+ final EntitySet state = new EntitySet();
+ state.add(delta);
+ return state;
+ }
+
+ /**
+ * Create an {@link EntitySet} based on {@link Contacts} specified by the
+ * given query parameters. This closes the {@link EntityIterator} when
+ * finished, so it doesn't subscribe to updates.
+ */
+ public static EntitySet fromQuery(ContentResolver resolver, String selection,
+ String[] selectionArgs, String sortOrder) {
+ EntityIterator iterator = null;
+ final EntitySet state = new EntitySet();
+ try {
+ // Perform background query to pull contact details
+ iterator = resolver.queryEntities(RawContacts.CONTENT_URI, selection, selectionArgs,
+ sortOrder);
+ while (iterator.hasNext()) {
+ // Read all contacts into local deltas to prepare for edits
+ final Entity before = iterator.next();
+ final EntityDelta entity = EntityDelta.fromBefore(before);
+ state.add(entity);
+ }
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Problem querying contact details", e);
+ } finally {
+ if (iterator != null) {
+ iterator.close();
+ }
+ }
+ return state;
+ }
+
+ /**
+ * Merge the "after" values from the given {@link EntitySet}.
+ */
+ public void mergeAfter(EntitySet remote) {
+ // TODO: write this folding logic to re-parent
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Build a list of {@link ContentProviderOperation} that will transform all
+ * the "before" {@link Entity} states into the modified state which all
+ * {@link EntityDelta} objects represent. This method specifically creates
+ * any {@link AggregationExceptions} rules needed to groups edits together.
+ */
+ public ArrayList<ContentProviderOperation> buildDiff() {
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+
+ final long rawContactId = this.findRawContactId();
+ int firstInsertRow = -1;
+
+ // First pass enforces versions remain consistent
+ for (EntityDelta delta : this) {
+ delta.buildAssert(diff);
+ }
+
+ final int assertMark = diff.size();
+
+ // Second pass builds actual operations
+ for (EntityDelta delta : this) {
+ final int firstBatch = diff.size();
+ delta.buildDiff(diff);
+
+ // Only create rules for inserts
+ if (!delta.isContactInsert()) continue;
+
+ if (rawContactId != -1) {
+ // Has existing contact, so bind to it strongly
+ final Builder builder = beginKeepTogether();
+ builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+ diff.add(builder.build());
+
+ } else if (firstInsertRow == -1) {
+ // First insert case, so record row
+ firstInsertRow = firstBatch;
+
+ } else {
+ // Additional insert case, so point at first insert
+ final Builder builder = beginKeepTogether();
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, firstInsertRow);
+ builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+ diff.add(builder.build());
+ }
+ }
+
+ // No real changes if only left with asserts
+ if (diff.size() == assertMark) {
+ diff.clear();
+ }
+
+ return diff;
+ }
+
+ /**
+ * Start building a {@link ContentProviderOperation} that will keep two
+ * {@link RawContacts} together.
+ */
+ protected Builder beginKeepTogether() {
+ final Builder builder = ContentProviderOperation
+ .newUpdate(AggregationExceptions.CONTENT_URI);
+ builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+ return builder;
+ }
+
+ /**
+ * Search all contained {@link EntityDelta} for the first one with an
+ * existing {@link RawContacts#_ID} value. Usually used when creating
+ * {@link AggregationExceptions} during an update.
+ */
+ public long findRawContactId() {
+ for (EntityDelta delta : this) {
+ final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
+ if (rawContactId != null && rawContactId >= 0) {
+ return rawContactId;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
+ */
+ public long getRawContactId(int index) {
+ if (index >=0 && index < this.size()) {
+ final EntityDelta delta = this.get(index);
+ return delta.getValues().getAsLong(RawContacts._ID);
+ } else {
+ return -1;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public int describeContents() {
+ // Nothing special about this parcel
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ public void writeToParcel(Parcel dest, int flags) {
+ final int size = this.size();
+ dest.writeInt(size);
+ for (EntityDelta delta : this) {
+ dest.writeParcelable(delta, flags);
+ }
+ }
+
+ public void readFromParcel(Parcel source) {
+ final int size = source.readInt();
+ for (int i = 0; i < size; i++) {
+ this.add(source.<EntityDelta> readParcelable(null));
+ }
+ }
+
+ public static final Parcelable.Creator<EntitySet> CREATOR = new Parcelable.Creator<EntitySet>() {
+ public EntitySet createFromParcel(Parcel in) {
+ final EntitySet state = new EntitySet();
+ state.readFromParcel(in);
+ return state;
+ }
+
+ public EntitySet[] newArray(int size) {
+ return new EntitySet[size];
+ }
+ };
+}
private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
+ // TODO: create group when it doesnt exist (syncadapter will fold in)
final ContentResolver resolver = context.getContentResolver();
final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { Groups.SOURCE_ID },
Groups.TITLE + "=?", new String[] { GOOGLE_MY_CONTACTS_GROUP }, null);
* The constants below are shared with the Exchange sync adapter, and are
* currently static. These values should be maintained in parallel.
*/
- private static final int TYPE_EMAIL1 = 20;
- private static final int TYPE_EMAIL2 = 21;
- private static final int TYPE_EMAIL3 = 22;
-
- private static final int TYPE_IM1 = 23;
- private static final int TYPE_IM2 = 24;
- private static final int TYPE_IM3 = 25;
-
private static final int TYPE_WORK2 = 26;
private static final int TYPE_HOME2 = 27;
private static final int TYPE_CAR = 28;
kind.actionHeader = new ActionInflater(list.resPackageName, kind);
kind.actionBody = new SimpleInflater(Email.DATA);
-
- kind.typeColumn = Email.TYPE;
- kind.typeList = Lists.newArrayList();
- kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1)
- .setSpecificMax(1));
- kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2)
- .setSpecificMax(1));
- kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3)
- .setSpecificMax(1));
+ kind.typeOverallMax = 3;
kind.fieldList = Lists.newArrayList();
kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
- kind.fieldList.add(new EditField(Email.DISPLAY_NAME, R.string.label_email_display_name,
- FLAGS_PERSON_NAME));
+// kind.fieldList.add(new EditField(Email.DISPLAY_NAME, R.string.label_email_display_name,
+// FLAGS_PERSON_NAME));
list.add(kind);
}
kind.actionHeader = new ActionInflater(list.resPackageName, kind);
kind.actionBody = new SimpleInflater(Im.DATA);
+ kind.typeOverallMax = 3;
- kind.typeColumn = Im.TYPE;
- kind.typeList = new ArrayList<EditType>();
- kind.typeList.add(new EditType(TYPE_IM1, R.string.type_im_1).
- setSpecificMax(1));
- kind.typeList.add(new EditType(TYPE_IM2, R.string.type_im_2).
- setSpecificMax(1));
- kind.typeList.add(new EditType(TYPE_IM3, R.string.type_im_3).
- setSpecificMax(1));
+ // NOTE: even though a traditional "type" exists, for editing
+ // purposes we're using the network to pick labels
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.typeColumn = Im.PROTOCOL;
+ kind.typeList = Lists.newArrayList();
+ kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim,
+ R.string.chat_aim));
+ kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn,
+ R.string.chat_msn));
+ kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo,
+ R.string.chat_yahoo));
+ kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype,
+ R.string.chat_skype));
+ kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq, R.string.chat_qq));
+ kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk,
+ R.string.chat_gtalk));
+ kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq,
+ R.string.chat_icq));
+ kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber,
+ R.string.chat_jabber));
+ kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom,
+ R.string.chat_other).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
kind.fieldList = Lists.newArrayList();
kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
public CharSequence inflateUsing(Context context, Cursor cursor) {
final EditType type = EntityModifier.getCurrentType(cursor, mKind);
final boolean validString = (type != null && type.actionRes != 0);
- CharSequence actionString;
+ if (!validString) return null;
+
if (type.customColumn != null) {
final int index = cursor.getColumnIndex(type.customColumn);
final String customLabel = cursor.getString(index);
- actionString = String.format(context.getString(type.actionRes),
- customLabel);
+ return String.format(context.getString(type.actionRes), customLabel);
} else {
- actionString = context.getText(type.actionRes);
+ return context.getText(type.actionRes);
}
- return validString ? actionString : null;
}
public CharSequence inflateUsing(Context context, ContentValues values) {
final EditType type = EntityModifier.getCurrentType(values, mKind);
final boolean validString = (type != null && type.actionRes != 0);
- CharSequence actionString;
+ if (!validString) return null;
+
if (type.customColumn != null) {
final String customLabel = values.getAsString(type.customColumn);
- actionString = String.format(context.getString(type.actionRes),
- customLabel);
+ return String.format(context.getString(type.actionRes), customLabel);
} else {
- actionString = context.getText(type.actionRes);
+ return context.getText(type.actionRes);
}
- return validString ? actionString : null;
}
}
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
import com.android.contacts.model.HardCodedSources;
import com.android.contacts.model.Sources;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Entity;
-import android.content.EntityIterator;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Contacts.Data;
// private long mSelectedRawContactId = -1;
// private long mContactId = -1;
+ private String mQuerySelection;
+
private ScrollingTabWidget mTabWidget;
private ContactHeaderWidget mHeader;
private ContactEditorView mEditor;
- private EditState mState = new EditState();
-
- private static class EditState extends ArrayList<EntityDelta> implements Parcelable {
- public long getContactId() {
- if (this.size() > 0) {
- // Assume the aggregate tied to first child
- final EntityDelta first = this.get(0);
- return first.getValues().getAsLong(RawContacts.CONTACT_ID);
- } else {
- // Otherwise return invalid value
- return -1;
- }
- }
-
- public long getRawContactId(int index) {
- if (index >=0 && index < this.size()) {
- final EntityDelta delta = this.get(index);
- return delta.getValues().getAsLong(RawContacts._ID);
- } else {
- return -1;
- }
- }
-
- /** {@inheritDoc} */
- public int describeContents() {
- // Nothing special about this parcel
- return 0;
- }
-
- /** {@inheritDoc} */
- public void writeToParcel(Parcel dest, int flags) {
- final int size = this.size();
- dest.writeInt(size);
- for (EntityDelta delta : this) {
- dest.writeParcelable(delta, flags);
- }
- }
-
- public void readFromParcel(Parcel source) {
- final int size = source.readInt();
- for (int i = 0; i < size; i++) {
- this.add(source.<EntityDelta> readParcelable(null));
- }
- }
-
- public static final Parcelable.Creator<EditState> CREATOR = new Parcelable.Creator<EditState>() {
- public EditState createFromParcel(Parcel in) {
- final EditState state = new EditState();
- state.readFromParcel(in);
- return state;
- }
-
- public EditState[] newArray(int size) {
- return new EditState[size];
- }
- };
- }
+ private EntitySet mState;
@Override
protected void onCreate(Bundle icicle) {
selection = RawContacts._ID + "=" + rawContactId;
}
- EntityIterator iterator = null;
- final EditState state = new EditState();
- try {
- // Perform background query to pull contact details
- iterator = resolver.queryEntities(RawContacts.CONTENT_URI,
- selection, null, null);
- while (iterator.hasNext()) {
- // Read all contacts into local deltas to prepare for edits
- final Entity before = iterator.next();
- final EntityDelta entity = EntityDelta.fromBefore(before);
- state.add(entity);
- }
- } catch (RemoteException e) {
- throw new IllegalStateException("Problem querying contact details", e);
- } finally {
- if (iterator != null) {
- iterator.close();
- }
- }
-
- target.mState = state;
+ target.mQuerySelection = selection;
+ target.mState = EntitySet.fromQuery(resolver, selection, null, null);
return null;
}
}
+
// /**
// * Instance state for {@link #mEditor} from a previous instance.
// */
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
// Read modifications from instance
- mState = savedInstanceState.<EditState> getParcelable(KEY_EDIT_STATE);
+ mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
// mSelectedRawContactId = savedInstanceState.getLong(KEY_SELECTED_TAB_ID);
// mContactId = savedInstanceState.getLong(KEY_CONTACT_ID);
* {@link EmptyService} to make sure the background thread can finish
* persisting in cases where the system wants to reclaim our process.
*/
- public static class PersistTask extends WeakAsyncTask<EditState, Void, Boolean, Context> {
- public PersistTask(Context context) {
- super(context);
+ public static class PersistTask extends
+ WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> {
+ private static final int PERSIST_TRIES = 3;
+
+ private static final int RESULT_UNCHANGED = 0;
+ private static final int RESULT_SUCCESS = 1;
+ private static final int RESULT_FAILURE = 2;
+
+ public PersistTask(EditContactActivity target) {
+ super(target);
}
/** {@inheritDoc} */
@Override
- protected void onPreExecute(Context context) {
+ protected void onPreExecute(EditContactActivity target) {
// Before starting this task, start an empty service to protect our
// process from being reclaimed by the system.
+ final Context context = target;
context.startService(new Intent(context, EmptyService.class));
}
/** {@inheritDoc} */
@Override
- protected Boolean doInBackground(Context context, EditState... params) {
- final EditState state = params[0];
- final ContentResolver resolver = context.getContentResolver();
-
- boolean savedChanges = false;
- for (EntityDelta entity : state) {
- // TODO: remove this extremely verbose debugging
- Log.d(TAG, "trying to persist " + entity.toString());
- final ArrayList<ContentProviderOperation> diff = entity.buildDiff();
-
- // Skip updates that don't change
- if (diff.size() == 0) continue;
- savedChanges = true;
-
- // TODO: handle failed operations by re-reading entity
- // may also need backoff algorithm to give failed msg after n tries
+ protected Integer doInBackground(EditContactActivity target, EntitySet... params) {
+ final ContentResolver resolver = target.getContentResolver();
+ int tries = 0;
+ Integer result = RESULT_FAILURE;
+ EntitySet state = params[0];
+ while (tries < PERSIST_TRIES) {
try {
+ // Build operations and try applying
+ final ArrayList<ContentProviderOperation> diff = state.buildDiff();
resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+ result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
+ break;
+
} catch (RemoteException e) {
- Log.w(TAG, "problem writing rawcontact diff", e);
+ // Something went wrong, bail without success
+ Log.e(TAG, "Problem persisting user edits", e);
+ break;
+
} catch (OperationApplicationException e) {
- Log.w(TAG, "problem writing rawcontact diff", e);
+ // Version consistency failed, re-parent change and try again
+ Log.w(TAG, "Version consistency failed, re-parenting", e);
+ final EntitySet newState = EntitySet.fromQuery(resolver,
+ target.mQuerySelection, null, null);
+ newState.mergeAfter(state);
+ state = newState;
}
}
- return savedChanges;
+ return result;
}
/** {@inheritDoc} */
@Override
- protected void onPostExecute(Context context, Boolean result) {
- if (result) {
+ protected void onPostExecute(EditContactActivity target, Integer result) {
+ final Context context = target;
+
+ if (result == RESULT_SUCCESS) {
Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+ } else if (result == RESULT_FAILURE) {
+ Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
}
// Stop the service that was protecting us
values.put(RawContacts.ACCOUNT_NAME, account.name);
values.put(RawContacts.ACCOUNT_TYPE, account.type);
- // Tie directly to an existing aggregate, which is turned
- // into an AggregationException later during persisting.
- final long aggregateId = target.mState.getContactId();
- if (aggregateId >= 0) {
- values.put(RawContacts.CONTACT_ID, aggregateId);
- }
-
// Parse any values from incoming intent
final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
final ContactsSource source = sources.getInflatedSource(account.type,
HardCodedSources.attemptMyContactsMembership(insert, target);
}
- target.mState.add(insert);
+ if (target.mState == null) {
+ // Create state if none exists yet
+ target.mState = EntitySet.fromSingle(insert);
+ } else {
+ // Add contact onto end of existing state
+ target.mState.add(insert);
+ }
target.bindTabs();
target.bindHeader();
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Sources;
import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.util.Constants;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
import com.android.internal.policy.PolicyManager;
private String[] mExcludeMimes;
/**
- * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
- * distinguishes actions that should initiate a text message.
- */
- // TODO: We should move this to someplace more general as it is needed in a
- // few places in the app code.
- public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
-
- private static final String SCHEME_TEL = "tel";
- private static final String SCHEME_SMSTO = "smsto";
- private static final String SCHEME_MAILTO = "mailto";
-
- /**
* Specific mime-types that should be bumped to the front of the fast-track.
* Other mime-types not appearing in this list follow in alphabetic order.
*/
private static final String[] ORDERED_MIMETYPES = new String[] {
Phone.CONTENT_ITEM_TYPE,
Contacts.CONTENT_ITEM_TYPE,
- MIME_SMS_ADDRESS,
+ Constants.MIME_SMS_ADDRESS,
Email.CONTENT_ITEM_TYPE,
};
mMimeType = mimeType;
// Inflate strings from cursor
- mAlternate = MIME_SMS_ADDRESS.equals(mimeType);
+ mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
if (mAlternate && mKind.actionAltHeader != null) {
mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
} else if (mKind.actionHeader != null) {
if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
final String number = getAsString(cursor, Phone.NUMBER);
if (!TextUtils.isEmpty(number)) {
- final Uri callUri = Uri.fromParts(SCHEME_TEL, number, null);
+ final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
}
- } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
+ } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
final String number = getAsString(cursor, Phone.NUMBER);
if (!TextUtils.isEmpty(number)) {
- final Uri smsUri = Uri.fromParts(SCHEME_SMSTO, number, null);
+ final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
}
} else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
final String address = getAsString(cursor, Email.DATA);
if (!TextUtils.isEmpty(address)) {
- final Uri mailUri = Uri.fromParts(SCHEME_MAILTO, address, null);
+ final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
}
// If phone number, also insert as text message action
if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
- final Action action = new DataAction(mContext, source, MIME_SMS_ADDRESS, kind,
- cursor);
- considerAdd(action, MIME_SMS_ADDRESS);
+ final Action action = new DataAction(mContext, source, Constants.MIME_SMS_ADDRESS,
+ kind, cursor);
+ considerAdd(action, Constants.MIME_SMS_ADDRESS);
}
}
--- /dev/null
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.app.Service;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+/**
+ * Background {@link Service} that is used to keep our process alive long enough
+ * for background threads to finish. Started and stopped directly by specific
+ * background tasks when needed.
+ */
+public class Constants {
+ /**
+ * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
+ * distinguishes actions that should initiate a text message.
+ */
+ public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
+
+ public static final String SCHEME_TEL = "tel";
+ public static final String SCHEME_SMSTO = "smsto";
+ public static final String SCHEME_MAILTO = "mailto";
+
+}
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
import static android.content.ContentProviderOperation.TYPE_INSERT;
import static android.content.ContentProviderOperation.TYPE_UPDATE;
*/
@LargeTest
public class EntityDeltaTests extends AndroidTestCase {
- public static final String TAG = "AugmentedEntityTests";
+ public static final String TAG = "EntityDeltaTests";
- private static final long TEST_CONTACT_ID = 12;
- private static final long TEST_PHONE_ID = 24;
+ public static final long TEST_CONTACT_ID = 12;
+ public static final long TEST_PHONE_ID = 24;
- private static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
- private static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+ public static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+ public static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
- private static final String TEST_ACCOUNT_NAME = "TEST";
+ public static final String TEST_ACCOUNT_NAME = "TEST";
public EntityDeltaTests() {
super();
mContext = getContext();
}
- protected Entity getEntity(long contactId, long phoneId) {
+ public static Entity getEntity(long contactId, long phoneId) {
// Build an existing contact read from database
final ContentValues contact = new ContentValues();
contact.put(RawContacts.VERSION, 43);
}
/**
- * Test that {@link EntityDelta#augmentTo(Parcel)} correctly passes any
- * changes through the {@link Parcel} object. This enforces that
- * {@link EntityDelta} should be identical when serialized against the
- * same "before" {@link Entity}.
+ * Test that {@link EntityDelta#mergeAfter(EntityDelta)} correctly passes
+ * any changes through the {@link Parcel} object. This enforces that
+ * {@link EntityDelta} should be identical when serialized against the same
+ * "before" {@link Entity}.
*/
public void testParcelChangesNone() {
final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
}
/**
- * Test that {@link ValuesDelta#buildDiff()} is correctly built for
- * insert, update, and delete cases. Note this only tests behavior for
- * individual {@link Data} rows.
+ * Test that {@link ValuesDelta#buildDiff(android.net.Uri)} is correctly
+ * built for insert, update, and delete cases. Note this only tests behavior
+ * for individual {@link Data} rows.
*/
public void testValuesDiffNone() {
final ContentValues before = new ContentValues();
}
/**
- * Test that {@link EntityDelta#buildDiff()} is correctly built for
+ * Test that {@link EntityDelta#buildDiff(ArrayList)} is correctly built for
* insert, update, and delete cases. This only tests a subset of possible
* {@link Data} row changes.
*/
final EntityDelta source = EntityDelta.fromBefore(before);
// Assert that writing unchanged produces few operations
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildDiff(diff);
assertTrue("Created changes when none needed", (diff.size() == 0));
}
source.addEntry(ValuesDelta.fromAfter(phone));
// Assert two operations: insert Data row and enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 4, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
source.addEntry(ValuesDelta.fromAfter(phone));
// Assert three operations: update Contact, insert Data row, enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 5, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
- // Assert two operations: update Data and enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ // Assert that version is enforced
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 4, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
source.getValues().markDeleted();
// Assert two operations: delete Contact and enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 2, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
final EntityDelta source = new EntityDelta(values);
// Assert two operations: delete Contact and enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 1, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
source.addEntry(ValuesDelta.fromAfter(phone));
// Assert two operations: delete Contact and enforce version
- final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ source.buildAssert(diff);
+ source.buildDiff(diff);
assertEquals("Unexpected operations", 2, diff.size());
{
final ContentProviderOperation oper = diff.get(0);
--- /dev/null
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link EntitySet} which focus on "diff" operations that should
+ * create {@link AggregationExceptions} in certain cases.
+ */
+@LargeTest
+public class EntitySetTests extends AndroidTestCase {
+ public static final String TAG = "EntitySetTests";
+
+ private static final long CONTACT_FIRST = 1;
+ private static final long CONTACT_SECOND = 2;
+
+ public EntitySetTests() {
+ super();
+ }
+
+ @Override
+ public void setUp() {
+ mContext = getContext();
+ }
+
+ protected EntityDelta getUpdate(long rawContactId) {
+ final Entity before = EntityDeltaTests.getEntity(rawContactId,
+ EntityDeltaTests.TEST_PHONE_ID);
+ return EntityDelta.fromBefore(before);
+ }
+
+ protected EntityDelta getInsert() {
+ final ContentValues after = new ContentValues();
+ after.put(RawContacts.ACCOUNT_NAME, EntityDeltaTests.TEST_ACCOUNT_NAME);
+ after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+ return new EntityDelta(values);
+ }
+
+ protected EntitySet setFrom(EntityDelta... deltas) {
+ final EntitySet set = EntitySet.fromSingle(deltas[0]);
+ for (int i = 1; i < deltas.length; i++) {
+ set.add(deltas[i]);
+ }
+ return set;
+ }
+
+ /**
+ * Count number of {@link AggregationExceptions} updates contained in the
+ * given list of {@link ContentProviderOperation}.
+ */
+ protected int countExceptionUpdates(ArrayList<ContentProviderOperation> diff) {
+ int updateCount = 0;
+ for (ContentProviderOperation oper : diff) {
+ if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
+ && oper.getType() == ContentProviderOperation.TYPE_UPDATE) {
+ updateCount++;
+ }
+ }
+ return updateCount;
+ }
+
+ public void testInsert() {
+ final EntityDelta insert = getInsert();
+ final EntitySet set = setFrom(insert);
+
+ // Inserting single shouldn't create rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 0, exceptionCount);
+ }
+
+ public void testUpdateUpdate() {
+ final EntityDelta updateFirst = getUpdate(CONTACT_FIRST);
+ final EntityDelta updateSecond = getUpdate(CONTACT_SECOND);
+ final EntitySet set = setFrom(updateFirst, updateSecond);
+
+ // Updating two existing shouldn't create rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 0, exceptionCount);
+ }
+
+ public void testUpdateInsert() {
+ final EntityDelta update = getUpdate(CONTACT_FIRST);
+ final EntityDelta insert = getInsert();
+ final EntitySet set = setFrom(update, insert);
+
+ // New insert should only create one rule
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 1, exceptionCount);
+ }
+
+ public void testInsertUpdateInsert() {
+ final EntityDelta insertFirst = getInsert();
+ final EntityDelta update = getUpdate(CONTACT_FIRST);
+ final EntityDelta insertSecond = getInsert();
+ final EntitySet set = setFrom(insertFirst, update, insertSecond);
+
+ // Two inserts should create two rules to bind against single existing
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 2, exceptionCount);
+ }
+
+ public void testInsertInsertInsert() {
+ final EntityDelta insertFirst = getInsert();
+ final EntityDelta insertSecond = getInsert();
+ final EntityDelta insertThird = getInsert();
+ final EntitySet set = setFrom(insertFirst, insertSecond, insertThird);
+
+ // Three new inserts should create only two binding rules
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ final int exceptionCount = countExceptionUpdates(diff);
+ assertEquals("Unexpected exception updates", 2, exceptionCount);
+ }
+}