auto import from //branches/cupcake/...@126645
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
new file mode 100755
index 0000000..8b25c09
--- /dev/null
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -0,0 +1,402 @@
+/*
+ * 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 android.app.ListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.Contacts;
+import android.provider.Contacts.Intents.Insert;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.telephony.TelephonyManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Displays the details of a specific call log entry.
+ */
+public class CallDetailActivity extends ListActivity implements
+ AdapterView.OnItemClickListener {
+ private static final String TAG = "CallDetail";
+
+ private Uri mUri;
+
+ private View mCallDetailItem;
+
+ private TextView mCallType;
+ private ImageView mCallTypeIcon;
+ private TextView mCallTime;
+ private View mCallDurationRow;
+ private TextView mCallDuration;
+
+ private String mNumber = null;
+
+ /* package */ LayoutInflater mInflater;
+ /* package */ Resources mResources;
+
+ static final String[] CALL_LOG_PROJECTION = new String[] {
+ CallLog.Calls.DATE,
+ CallLog.Calls.DURATION,
+ CallLog.Calls.NUMBER,
+ CallLog.Calls.TYPE,
+ };
+
+ static final int DATE_COLUMN_INDEX = 0;
+ static final int DURATION_COLUMN_INDEX = 1;
+ static final int NUMBER_COLUMN_INDEX = 2;
+ static final int CALL_TYPE_COLUMN_INDEX = 3;
+
+ static final String[] PHONES_PROJECTION = new String[] {
+ Phones.PERSON_ID,
+ };
+
+ static final int PERSON_ID_COLUMN_INDEX = 0;
+
+ private static final int INVALID_TYPE = -1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.call_detail);
+
+ mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
+ mResources = getResources();
+
+ mCallDetailItem = mInflater.inflate(R.layout.call_detail_item, getListView(), false);
+
+ mCallType = (TextView) mCallDetailItem.findViewById(R.id.call_type);
+ mCallTypeIcon = (ImageView) mCallDetailItem.findViewById(R.id.call_type_icon);
+ mCallTime = (TextView) mCallDetailItem.findViewById(R.id.call_time);
+ mCallDurationRow = mCallDetailItem.findViewById(R.id.call_duration_row);
+ mCallDuration = (TextView) mCallDetailItem.findViewById(R.id.call_duration);
+
+ getListView().setOnItemClickListener(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateData(getIntent().getData());
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL: {
+ // Make sure phone isn't already busy before starting direct call
+ TelephonyManager tm = (TelephonyManager)
+ getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
+ Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", mNumber, null));
+ startActivity(callIntent);
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * Try a reverse-phonebook lookup to find the contact, if any, behind the given number.
+ *
+ * @param number Phone number to perform reverse-lookup against
+ * @return Uri into {@link Contacts.People} if found, otherwise null
+ */
+ private Uri getPersonUri(String number) {
+ Uri personUri = null;
+
+ // Perform a reverse-phonebook lookup to find the PERSON_ID
+ ContentResolver resolver = getContentResolver();
+ Uri phoneUri = Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, Uri.encode(number));
+ Cursor phonesCursor = resolver.query(phoneUri, PHONES_PROJECTION, null, null, null);
+ try {
+ if (phonesCursor != null && phonesCursor.moveToFirst()) {
+ long personId = phonesCursor.getLong(PERSON_ID_COLUMN_INDEX);
+ personUri = ContentUris.withAppendedId(Contacts.People.CONTENT_URI, personId);
+ }
+ } finally {
+ if (phonesCursor != null) {
+ phonesCursor.close();
+ }
+ }
+
+ return personUri;
+ }
+
+ /**
+ * Update user interface with details of given call.
+ *
+ * @param callUri Uri into {@link CallLog.Calls}
+ */
+ private void updateData(Uri callUri) {
+ ContentResolver resolver = getContentResolver();
+ Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null);
+ try {
+ if (callCursor != null && callCursor.moveToFirst()) {
+ // Read call log specifics
+ mNumber = callCursor.getString(NUMBER_COLUMN_INDEX);
+ long date = callCursor.getLong(DATE_COLUMN_INDEX);
+ long duration = callCursor.getLong(DURATION_COLUMN_INDEX);
+ int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX);
+
+ // Pull out string in format [relative], [date]
+ CharSequence dateClause = DateUtils.formatDateRange(this, date, date,
+ DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE |
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR |
+ DateUtils.FORMAT_ABBREV_ALL);
+ long now = System.currentTimeMillis();
+ CharSequence relativeClause = DateUtils.getRelativeTimeSpanString(date, now,
+ DateUtils.SECOND_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
+ String dateString = getString(R.string.datetime_relative,
+ dateClause, relativeClause);
+ mCallTime.setText(dateString);
+
+ // Set the duration
+ if (callType == Calls.MISSED_TYPE) {
+ mCallDurationRow.setVisibility(View.GONE);
+ } else {
+ mCallDurationRow.setVisibility(View.VISIBLE);
+ mCallDuration.setText(DateUtils.formatElapsedTime(duration));
+ }
+
+ // Set the call type icon and caption
+ switch (callType) {
+ case Calls.INCOMING_TYPE:
+ mCallTypeIcon.setImageResource(android.R.drawable.sym_call_incoming);
+ mCallType.setText(R.string.type_incoming);
+ break;
+
+ case Calls.OUTGOING_TYPE:
+ mCallTypeIcon.setImageResource(android.R.drawable.sym_call_outgoing);
+ mCallType.setText(R.string.type_outgoing);
+ break;
+
+ case Calls.MISSED_TYPE:
+ mCallTypeIcon.setImageResource(android.R.drawable.sym_call_missed);
+ mCallType.setText(R.string.type_missed);
+ break;
+ }
+
+ // Build list of various available actions
+ List<ViewEntry> actions = new LinkedList<ViewEntry>();
+
+ Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", mNumber, null));
+ actions.add(new ViewEntry(R.drawable.ic_dialer_fork_current_call,
+ getString(R.string.recentCalls_callNumber, mNumber), callIntent));
+
+ Intent smsIntent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts("sms", mNumber, null));
+ actions.add(new ViewEntry(R.drawable.sym_action_sms,
+ getString(R.string.menu_sendTextMessage), smsIntent));
+
+ // Let user view contact details if they exist, otherwise add option
+ // to create new contact from this number.
+ Uri personUri = getPersonUri(mNumber);
+
+ if (personUri != null) {
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW, personUri);
+ actions.add(new ViewEntry(R.drawable.ic_tab_unselected_contacts,
+ getString(R.string.menu_viewContact), viewIntent));
+ } else {
+ Intent createIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ createIntent.setType(People.CONTENT_ITEM_TYPE);
+ createIntent.putExtra(Insert.PHONE, mNumber);
+ actions.add(new ViewEntry(R.drawable.ic_dialer_fork_add_call,
+ getString(R.string.recentCalls_addToContact), createIntent));
+ }
+
+ ViewAdapter adapter = new ViewAdapter(this, mCallDetailItem, actions);
+ setListAdapter(adapter);
+ } else {
+ // Something went wrong reading in our primary data, so we're going to
+ // bail out and show error to users.
+ Toast.makeText(this, R.string.toast_call_detail_error,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ } finally {
+ if (callCursor != null) {
+ callCursor.close();
+ }
+ }
+ }
+
+ static final class ViewEntry {
+ public int icon = -1;
+ public String text = null;
+ public Intent intent = null;
+
+ public ViewEntry(int icon, String text, Intent intent) {
+ this.icon = icon;
+ this.text = text;
+ this.intent = intent;
+ }
+ }
+
+ static final class ViewAdapter extends BaseAdapter {
+
+ private final View mCallDetailItem;
+ private final List<ViewEntry> mActions;
+
+ private final Context mContext;
+ private final LayoutInflater mInflater;
+
+ public ViewAdapter(Context context, View callDetailItem, List<ViewEntry> actions) {
+ mCallDetailItem = callDetailItem;
+ mActions = actions;
+
+ mContext = context;
+ mInflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ }
+
+ public int getCount() {
+ // Count is actions plus two headers and call details panel.
+ return mActions.size() + 2;
+ }
+
+ public Object getItem(int position) {
+ if (position >= POS_FIRST_ITEM) {
+ return mActions.get(position - POS_FIRST_ITEM);
+ }
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ private static final int TYPE_HEADER = 0;
+ private static final int TYPE_CALL_DETAILS = 1;
+ private static final int TYPE_ACTION = 2;
+
+ private static final int POS_CALL_DETAILS = 0;
+ private static final int POS_ACTIONS_HEADER = 1;
+ private static final int POS_FIRST_ITEM = 2;
+
+ public int getViewTypeCount() {
+ // Types are headers, call details panel, and actions.
+ return 3;
+ }
+
+ public int getItemViewType(int position) {
+ switch(position) {
+ case POS_CALL_DETAILS:
+ return TYPE_CALL_DETAILS;
+ case POS_ACTIONS_HEADER:
+ return TYPE_HEADER;
+ default:
+ return TYPE_ACTION;
+ }
+ }
+
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ public boolean isEnabled(int position) {
+ return (position > POS_ACTIONS_HEADER);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Make sure we have a valid convertView to start with
+ if (convertView == null) {
+ switch(getItemViewType(position)) {
+ case TYPE_HEADER: {
+ convertView = mInflater.inflate(R.layout.list_separator, parent, false);
+ break;
+ }
+ case TYPE_CALL_DETAILS: {
+ convertView = mCallDetailItem;
+ break;
+ }
+ case TYPE_ACTION: {
+ convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item,
+ parent, false);
+ break;
+ }
+ }
+ }
+
+ // Now fill our known-good convertView with data
+ switch(position) {
+ case POS_CALL_DETAILS: {
+ // Assume mCallDetailItem is already filled with correct data.
+ break;
+ }
+ case POS_ACTIONS_HEADER: {
+ TextView textView = (TextView) convertView;
+ textView.setText(mContext.getResources().getString(
+ R.string.header_actions));
+ break;
+ }
+ default: {
+ // Fill action with icon and text.
+ ViewEntry entry = (ViewEntry) getItem(position);
+ convertView.setTag(entry);
+
+ ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
+ TextView text = (TextView) convertView.findViewById(R.id.text);
+
+ icon.setImageResource(entry.icon);
+ text.setText(entry.text);
+
+ break;
+ }
+ }
+
+ return convertView;
+ }
+
+ }
+
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ // Handle passing action off to correct handler.
+ if (view.getTag() instanceof ViewEntry) {
+ ViewEntry entry = (ViewEntry) view.getTag();
+ if (entry.intent != null) {
+ startActivity(entry.intent);
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/EditContactActivity.java b/src/com/android/contacts/EditContactActivity.java
index 2994ce4..cdf2cef 100644
--- a/src/com/android/contacts/EditContactActivity.java
+++ b/src/com/android/contacts/EditContactActivity.java
@@ -74,7 +74,9 @@
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.telephony.PhoneNumberFormattingTextWatcher;
+import android.text.Editable;
import android.text.TextUtils;
+import android.text.TextWatcher;
import android.text.method.DialerKeyListener;
import android.text.method.TextKeyListener;
import android.text.method.TextKeyListener.Capitalize;
@@ -90,6 +92,7 @@
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.CheckBox;
+import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ExpandableListView;
import android.widget.ImageView;
@@ -110,7 +113,7 @@
* background while this activity is running, the updates will be overwritten.
*/
public final class EditContactActivity extends Activity implements View.OnClickListener,
- ExpandableListView.OnChildClickListener {
+ ExpandableListView.OnChildClickListener, TextWatcher, CheckBox.OnCheckedChangeListener {
private static final String TAG = "EditContactActivity";
private static final int STATE_UNKNOWN = 0;
@@ -177,6 +180,9 @@
private EditText mPhoneticNameView;
private LinearLayout mPhoneticNameLayout;
+ /** Flag marking this contact as changed, meaning we should write changes back. */
+ private boolean mContactChanged = false;
+
// These are accessed by inner classes. They're package scoped to make access more efficient.
/* package */ ContentResolver mResolver;
/* package */ ArrayList<EditEntry> mPhoneEntries = new ArrayList<EditEntry>();
@@ -395,6 +401,7 @@
outState.putBoolean("photoChanged", mPhotoChanged);
outState.putBoolean("sendToVoicemail", mSendToVoicemailCheckBox.isChecked());
outState.putString("phoneticName", mPhoneticNameView.getText().toString());
+ outState.putBoolean("contactChanged", mContactChanged);
}
@Override
@@ -421,6 +428,7 @@
mPhotoChanged = inState.getBoolean("photoChanged");
mSendToVoicemailCheckBox.setChecked(inState.getBoolean("sendToVoicemail"));
mPhoneticNameView.setText(inState.getString("phoneticName"));
+ mContactChanged = inState.getBoolean("contactChanged");
// Now that everything is restored, build the view
buildViews();
@@ -448,6 +456,7 @@
case RINGTONE_PICKED: {
Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
handleRingtonePicked(pickedUri);
+ mContactChanged = true;
break;
}
}
@@ -754,6 +763,7 @@
// Rebuild the views if needed
if (entry != null) {
buildViews();
+ mContactChanged = true;
View dataView = entry.view.findViewById(R.id.data);
if (dataView == null) {
@@ -897,6 +907,8 @@
public void onClick(DialogInterface dialog, int which) {
entry.setLabel(EditContactActivity.this, ContactMethods.TYPE_CUSTOM,
label.getText().toString());
+ mContactChanged = true;
+
if (addTo != null) {
addTo.add(entry);
buildViews();
@@ -999,7 +1011,11 @@
// Add the entry to the my contacts group if it isn't there already
People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
setResult(RESULT_OK, new Intent().setData(mUri));
- Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+
+ // Only notify user if we actually changed contact
+ if (mContactChanged || mPhotoChanged) {
+ Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+ }
}
}
@@ -1108,6 +1124,7 @@
// Name
mNameView.setText(personCursor.getString(CONTACT_NAME_COLUMN));
+ mNameView.addTextChangedListener(this);
// Photo
mPhoto = People.loadContactPhoto(this, mUri, 0, null);
@@ -1121,6 +1138,8 @@
// Send to voicemail
mSendToVoicemailCheckBox
.setChecked(personCursor.getInt(CONTACT_SEND_TO_VOICEMAIL_COLUMN) == 1);
+ mSendToVoicemailCheckBox
+ .setOnCheckedChangeListener(this);
// Organizations
Uri organizationsUri = Uri.withAppendedPath(mUri, Organizations.CONTENT_DIRECTORY);
@@ -1158,6 +1177,7 @@
// Phonetic name
mPhoneticNameView.setText(personCursor.getString(CONTACT_PHONETIC_NAME_COLUMN));
+ mPhoneticNameView.addTextChangedListener(this);
personCursor.close();
@@ -1269,6 +1289,8 @@
entry.isPrimary = true;
mEmailEntries.add(entry);
}
+
+ mContactChanged = false;
}
/**
@@ -1562,6 +1584,14 @@
}
}
+ // Connect listeners up to watch for changed values.
+ if (data instanceof EditText) {
+ data.addTextChangedListener(this);
+ }
+ if (data2 instanceof EditText) {
+ data2.addTextChangedListener(this);
+ }
+
// Hook up the delete button
View delete = view.findViewById(R.id.delete);
if (delete != null) delete.setOnClickListener(this);
@@ -1597,9 +1627,11 @@
createCustomPicker(mEntry, null);
} else {
mEntry.setLabel(EditContactActivity.this, type, mLabels[which]);
+ mContactChanged = true;
}
} else {
mEntry.setLabel(EditContactActivity.this, which, mLabels[which]);
+ mContactChanged = true;
}
}
}
@@ -1847,7 +1879,13 @@
break;
}
- values.put(ContactMethods.ISPRIMARY, isPrimary ? "1" : "0");
+ // Only set the ISPRIMARY flag if part of the incoming data. This is because the
+ // ContentProvider will try finding a new primary when setting to false, meaning
+ // it's possible to lose primary altogether as we walk down the list. If this editor
+ // implements editing of primaries in the future, this will need to be revisited.
+ if (isPrimary) {
+ values.put(ContactMethods.ISPRIMARY, 1);
+ }
// Save the data
if (view != null && syncDataWithView) {
@@ -2029,4 +2067,22 @@
return entry;
}
}
+
+ public void afterTextChanged(Editable s) {
+ // Someone edited a text field, so assume this contact is dirty.
+ mContactChanged = true;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing; editing handled by afterTextChanged()
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing; editing handled by afterTextChanged()
+ }
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ // Someone changed a checkbox, so assume this contact is dirty.
+ mContactChanged = true;
+ }
}
diff --git a/src/com/android/contacts/RecentCallsListActivity.java b/src/com/android/contacts/RecentCallsListActivity.java
index 85099fa..8298d21 100644
--- a/src/com/android/contacts/RecentCallsListActivity.java
+++ b/src/com/android/contacts/RecentCallsListActivity.java
@@ -32,6 +32,7 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
@@ -433,11 +434,12 @@
views.durationView.setText(DateUtils.formatElapsedTime(c.getLong(DURATION_COLUMN_INDEX)));
}
- // Set the time and date
+ // Set the date/time field by mixing relative and absolute times.
int flags = DateUtils.FORMAT_ABBREV_RELATIVE | DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_ABBREV_MONTH;
+
views.dateView.setText(DateUtils.getRelativeDateTimeString(context, date,
- DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, flags));
+ DateUtils.MINUTE_IN_MILLIS, DateUtils.DAY_IN_MILLIS * 2, flags));
// Set the icon
switch (type) {
@@ -815,6 +817,8 @@
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
- callEntry(position);
+ Intent intent = new Intent(this, CallDetailActivity.class);
+ intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
+ startActivity(intent);
}
}