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);
     }
 }