Merge "Adds Calendar events to the Recent card"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e2aad85..61010fa 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -51,6 +51,7 @@
     <uses-permission android:name="android.permission.REBOOT" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.READ_SMS" />
+    <uses-permission android:name="android.permission.READ_CALENDAR" />
 
     <application
         android:name="com.android.contacts.ContactsApplication"
diff --git a/res/drawable-hdpi/ic_event_24dp.png b/res/drawable-hdpi/ic_event_24dp.png
new file mode 100644
index 0000000..023695a
--- /dev/null
+++ b/res/drawable-hdpi/ic_event_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_event_24dp.png b/res/drawable-mdpi/ic_event_24dp.png
new file mode 100644
index 0000000..f5abeb7
--- /dev/null
+++ b/res/drawable-mdpi/ic_event_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_event_24dp.png b/res/drawable-xhdpi/ic_event_24dp.png
new file mode 100644
index 0000000..a2bd4b2
--- /dev/null
+++ b/res/drawable-xhdpi/ic_event_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_event_24dp.png b/res/drawable-xxhdpi/ic_event_24dp.png
new file mode 100644
index 0000000..f27a424
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_event_24dp.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c1bd3c0..696ea08 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -654,14 +654,25 @@
     <!-- Title of recent card. [CHAR LIMIT=60] -->
     <string name="recent_card_title">Recent</string>
 
-    <!-- Timestamp string for interactions from yesterday. [CHAR LIMIT=40] -->
-    <string name="timestamp_string_yesterday">Yesterday</string>
-    <!-- Timestamp string for interactions from tomorrow. [CHAR LIMIT=40] -->
-    <string name="timestamp_string_tomorrow">Tomorrow</string>
-
     <!-- Title of sms action entry. [CHAR LIMIT=60] -->
     <string name="send_message">Send message</string>
 
     <!-- Toast that appears when you are copying a directory contact into your personal contacts -->
     <string name="toast_making_personal_copy">Creating a personal copy...</string>
+    <!-- Timestamp string for interactions from yesterday. [CHAR LIMIT=40] -->
+    <string name="yesterday">Yesterday</string>
+    <string name="tomorrow">Tomorrow</string>
+    <!-- Timestamp string for interactions from today. [CHAR LIMIT=40] -->
+    <string name="today">Today</string>
+    <!-- Text for an event starting on the current day with a start and end time.
+         For ex, "Today at 5:00pm-6:00pm" [CHAR LIMIT=NONE] -->
+    <string name="today_at_time_fmt">"Today at <xliff:g id="time_interval">%s</xliff:g>"</string>
+    <!-- Text for an event starting on the next day with a start and end time.
+         For ex, "Tomorrow at 5:00pm-6:00pm" [CHAR LIMIT=NONE] -->
+    <string name="tomorrow_at_time_fmt">"Tomorrow at <xliff:g id="time_interval">%s</xliff:g>"</string>
+    <!-- Format string for a date and time description.  For ex:
+         "April 19, 2012, 3:00pm - 4:00pm" [CHAR LIMIT=NONE] -->
+    <string name="date_time_fmt">"<xliff:g id="date">%s</xliff:g>, <xliff:g id="time_interval">%s</xliff:g>"</string>
+    <!-- Title for untitled calendar interactions [CHAR LIMIT=40] -->
+    <string name="untitled_event">(Untitled event)</string>
 </resources>
diff --git a/src/com/android/contacts/interactions/CalendarInteraction.java b/src/com/android/contacts/interactions/CalendarInteraction.java
new file mode 100644
index 0000000..68e37f7
--- /dev/null
+++ b/src/com/android/contacts/interactions/CalendarInteraction.java
@@ -0,0 +1,269 @@
+package com.android.contacts.interactions;
+
+import com.android.contacts.R;
+
+import android.content.ContentValues;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Events;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+
+/**
+ * Represents a calendar event interaction, wrapping the columns in
+ * {@link android.provider.CalendarContract.Attendees}.
+ */
+public class CalendarInteraction implements ContactInteraction {
+    private static final String TAG = CalendarInteraction.class.getSimpleName();
+
+    private static final int CALENDAR_ICON_RES = R.drawable.ic_event_24dp;
+
+    private ContentValues mValues;
+
+    public CalendarInteraction(ContentValues values) {
+        mValues = values;
+    }
+
+    @Override
+    public Intent getIntent() {
+        return new Intent(Intent.ACTION_VIEW).setData(
+                ContentUris.withAppendedId(Events.CONTENT_URI, getEventId()));
+    }
+
+    @Override
+    public long getInteractionDate() {
+        return getDtstart();
+    }
+
+    @Override
+    public String getViewHeader(Context context) {
+        String title = getTitle();
+        if (TextUtils.isEmpty(title)) {
+            return context.getResources().getString(R.string.untitled_event);
+        }
+        return title;
+    }
+
+    @Override
+    public String getViewBody(Context context) {
+        return null;
+    }
+
+    @Override
+    public String getViewFooter(Context context) {
+        // Pulled from com.android.calendar.EventInfoFragment.updateEvent(View view)
+        // TODO: build callback to update time zone if different than preferences
+        String localTimezone = Time.getCurrentTimezone();
+
+        String displayedDatetime = CalendarInteractionUtils.getDisplayedDatetime(
+                getDtstart(), getDtend(), System.currentTimeMillis(), localTimezone,
+                getAllDay(), context);
+
+        return displayedDatetime;
+    }
+
+    @Override
+    public Drawable getIcon(Context context) {
+        return context.getResources().getDrawable(CALENDAR_ICON_RES);
+    }
+
+    @Override
+    public Drawable getBodyIcon(Context context) {
+        return null;
+    }
+
+    @Override
+    public Drawable getFooterIcon(Context context) {
+        return null;
+    }
+
+    public String getAttendeeEmail() {
+        return mValues.getAsString(Attendees.ATTENDEE_EMAIL);
+    }
+
+    public String getAttendeeIdentity() {
+        return mValues.getAsString(Attendees.ATTENDEE_IDENTITY);
+    }
+
+    public String getAttendeeIdNamespace() {
+        return mValues.getAsString(Attendees.ATTENDEE_ID_NAMESPACE);
+    }
+
+    public String getAttendeeName() {
+        return mValues.getAsString(Attendees.ATTENDEE_NAME);
+    }
+
+    public int getAttendeeRelationship() {
+        return mValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+    }
+
+    public int getAttendeeStatus() {
+        return mValues.getAsInteger(Attendees.ATTENDEE_STATUS);
+    }
+
+    public int getAttendeeType() {
+        return mValues.getAsInteger(Attendees.ATTENDEE_TYPE);
+    }
+
+    public int getEventId() {
+        return mValues.getAsInteger(Attendees.EVENT_ID);
+    }
+
+    public int getAccessLevel() {
+        return mValues.getAsInteger(Attendees.ACCESS_LEVEL);
+    }
+
+    public boolean getAllDay() {
+        return mValues.getAsBoolean(Attendees.ALL_DAY);
+    }
+
+    public int getAvailability() {
+        return mValues.getAsInteger(Attendees.AVAILABILITY);
+    }
+
+    public int getCalendarId() {
+        return mValues.getAsInteger(Attendees.CALENDAR_ID);
+    }
+
+    public boolean getCanInviteOthers() {
+        return mValues.getAsBoolean(Attendees.CAN_INVITE_OTHERS);
+    }
+
+    public String getCustomAppPackage() {
+        return mValues.getAsString(Attendees.CUSTOM_APP_PACKAGE);
+    }
+
+    public String getCustomAppUri() {
+        return mValues.getAsString(Attendees.CUSTOM_APP_URI);
+    }
+
+    public String getDescription() {
+        return mValues.getAsString(Attendees.DESCRIPTION);
+    }
+
+    public int getDisplayColor() {
+        return mValues.getAsInteger(Attendees.DISPLAY_COLOR);
+    }
+
+    public long getDtend() {
+        return mValues.getAsLong(Attendees.DTEND);
+    }
+
+    public long getDtstart() {
+        return mValues.getAsLong(Attendees.DTSTART);
+    }
+
+    public String getDuration() {
+        return mValues.getAsString(Attendees.DURATION);
+    }
+
+    public int getEventColor() {
+        return mValues.getAsInteger(Attendees.EVENT_COLOR);
+    }
+
+    public String getEventColorKey() {
+        return mValues.getAsString(Attendees.EVENT_COLOR_KEY);
+    }
+
+    public String getEventEndTimezone() {
+        return mValues.getAsString(Attendees.EVENT_END_TIMEZONE);
+    }
+
+    public String getEventLocation() {
+        return mValues.getAsString(Attendees.EVENT_LOCATION);
+    }
+
+    public String getExdate() {
+        return mValues.getAsString(Attendees.EXDATE);
+    }
+
+    public String getExrule() {
+        return mValues.getAsString(Attendees.EXRULE);
+    }
+
+    public boolean getGuestsCanInviteOthers() {
+        return mValues.getAsBoolean(Attendees.GUESTS_CAN_INVITE_OTHERS);
+    }
+
+    public boolean getGuestsCanModify() {
+        return mValues.getAsBoolean(Attendees.GUESTS_CAN_MODIFY);
+    }
+
+    public boolean getGuestsCanSeeGuests() {
+        return mValues.getAsBoolean(Attendees.GUESTS_CAN_SEE_GUESTS);
+    }
+
+    public boolean getHasAlarm() {
+        return mValues.getAsBoolean(Attendees.HAS_ALARM);
+    }
+
+    public boolean getHasAttendeeData() {
+        return mValues.getAsBoolean(Attendees.HAS_ATTENDEE_DATA);
+    }
+
+    public boolean getHasExtendedProperties() {
+        return mValues.getAsBoolean(Attendees.HAS_EXTENDED_PROPERTIES);
+    }
+
+    public String getIsOrganizer() {
+        return mValues.getAsString(Attendees.IS_ORGANIZER);
+    }
+
+    public long getLastDate() {
+        return mValues.getAsLong(Attendees.LAST_DATE);
+    }
+
+    public boolean getLastSynced() {
+        return mValues.getAsBoolean(Attendees.LAST_SYNCED);
+    }
+
+    public String getOrganizer() {
+        return mValues.getAsString(Attendees.ORGANIZER);
+    }
+
+    public boolean getOriginalAllDay() {
+        return mValues.getAsBoolean(Attendees.ORIGINAL_ALL_DAY);
+    }
+
+    public String getOriginalId() {
+        return mValues.getAsString(Attendees.ORIGINAL_ID);
+    }
+
+    public long getOriginalInstanceTime() {
+        return mValues.getAsLong(Attendees.ORIGINAL_INSTANCE_TIME);
+    }
+
+    public String getOriginalSyncId() {
+        return mValues.getAsString(Attendees.ORIGINAL_SYNC_ID);
+    }
+
+    public String getRdate() {
+        return mValues.getAsString(Attendees.RDATE);
+    }
+
+    public String getRrule() {
+        return mValues.getAsString(Attendees.RRULE);
+    }
+
+    public int getSelfAttendeeStatus() {
+        return mValues.getAsInteger(Attendees.SELF_ATTENDEE_STATUS);
+    }
+
+    public int getStatus() {
+        return mValues.getAsInteger(Attendees.STATUS);
+    }
+
+    public String getTitle() {
+        return mValues.getAsString(Attendees.TITLE);
+    }
+
+    public String getUid2445() {
+        return mValues.getAsString(Attendees.UID_2445);
+    }
+}
diff --git a/src/com/android/contacts/interactions/CalendarInteractionUtils.java b/src/com/android/contacts/interactions/CalendarInteractionUtils.java
new file mode 100644
index 0000000..c7943f0
--- /dev/null
+++ b/src/com/android/contacts/interactions/CalendarInteractionUtils.java
@@ -0,0 +1,192 @@
+package com.android.contacts.interactions;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * The following methods were pulled from
+ * {@link com.android.calendar.EventInfoFragment.updateEvent(View view)}
+ * TODO: Move this to frameworks/opt
+ */
+public class CalendarInteractionUtils {
+
+    // Using int constants as a return value instead of an enum to minimize resources.
+    private static final int TODAY = 1;
+    private static final int TOMORROW = 2;
+    private static final int NONE = 0;
+
+    /**
+     * Returns a string description of the specified time interval.
+     */
+    public static String getDisplayedDatetime(long startMillis, long endMillis, long currentMillis,
+            String localTimezone, boolean allDay, Context context) {
+        // Configure date/time formatting.
+        int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY;
+        int flagsTime = DateUtils.FORMAT_SHOW_TIME;
+        if (DateFormat.is24HourFormat(context)) {
+            flagsTime |= DateUtils.FORMAT_24HOUR;
+        }
+
+        Time currentTime = new Time(localTimezone);
+        currentTime.set(currentMillis);
+        Resources resources = context.getResources();
+        String datetimeString = null;
+        if (allDay) {
+            // All day events require special timezone adjustment.
+            long localStartMillis = convertAlldayUtcToLocal(null, startMillis, localTimezone);
+            long localEndMillis = convertAlldayUtcToLocal(null, endMillis, localTimezone);
+            if (singleDayEvent(localStartMillis, localEndMillis, currentTime.gmtoff)) {
+                // If possible, use "Today" or "Tomorrow" instead of a full date string.
+                int todayOrTomorrow = isTodayOrTomorrow(context.getResources(),
+                        localStartMillis, currentMillis, currentTime.gmtoff);
+                if (TODAY == todayOrTomorrow) {
+                    datetimeString = resources.getString(R.string.today);
+                } else if (TOMORROW == todayOrTomorrow) {
+                    datetimeString = resources.getString(R.string.tomorrow);
+                }
+            }
+            if (datetimeString == null) {
+                // For multi-day allday events or single-day all-day events that are not
+                // today or tomorrow, use framework formatter.
+                Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault());
+                datetimeString = DateUtils.formatDateRange(context, f, startMillis,
+                        endMillis, flagsDate, Time.TIMEZONE_UTC).toString();
+            }
+        } else {
+            if (singleDayEvent(startMillis, endMillis, currentTime.gmtoff)) {
+                // Format the time.
+                String timeString = formatDateRange(context, startMillis, endMillis,
+                        flagsTime);
+
+                // If possible, use "Today" or "Tomorrow" instead of a full date string.
+                int todayOrTomorrow = isTodayOrTomorrow(context.getResources(), startMillis,
+                        currentMillis, currentTime.gmtoff);
+                if (TODAY == todayOrTomorrow) {
+                    // Example: "Today at 1:00pm - 2:00 pm"
+                    datetimeString = resources.getString(R.string.today_at_time_fmt,
+                            timeString);
+                } else if (TOMORROW == todayOrTomorrow) {
+                    // Example: "Tomorrow at 1:00pm - 2:00 pm"
+                    datetimeString = resources.getString(R.string.tomorrow_at_time_fmt,
+                            timeString);
+                } else {
+                    // Format the full date. Example: "Thursday, April 12, 1:00pm - 2:00pm"
+                    String dateString = formatDateRange(context, startMillis, endMillis,
+                            flagsDate);
+                    datetimeString = resources.getString(R.string.date_time_fmt, dateString,
+                            timeString);
+                }
+            } else {
+                // For multiday events, shorten day/month names.
+                // Example format: "Fri Apr 6, 5:00pm - Sun, Apr 8, 6:00pm"
+                int flagsDatetime = flagsDate | flagsTime | DateUtils.FORMAT_ABBREV_MONTH |
+                        DateUtils.FORMAT_ABBREV_WEEKDAY;
+                datetimeString = formatDateRange(context, startMillis, endMillis,
+                        flagsDatetime);
+            }
+        }
+        return datetimeString;
+    }
+
+    /**
+     * Convert given UTC time into current local time. This assumes it is for an
+     * allday event and will adjust the time to be on a midnight boundary.
+     *
+     * @param recycle Time object to recycle, otherwise null.
+     * @param utcTime Time to convert, in UTC.
+     * @param tz The time zone to convert this time to.
+     */
+    private static long convertAlldayUtcToLocal(Time recycle, long utcTime, String tz) {
+        if (recycle == null) {
+            recycle = new Time();
+        }
+        recycle.timezone = Time.TIMEZONE_UTC;
+        recycle.set(utcTime);
+        recycle.timezone = tz;
+        return recycle.normalize(true);
+    }
+
+    public static long convertAlldayLocalToUTC(Time recycle, long localTime, String tz) {
+        if (recycle == null) {
+            recycle = new Time();
+        }
+        recycle.timezone = tz;
+        recycle.set(localTime);
+        recycle.timezone = Time.TIMEZONE_UTC;
+        return recycle.normalize(true);
+    }
+
+    /**
+     * Returns whether the specified time interval is in a single day.
+     */
+    private static boolean singleDayEvent(long startMillis, long endMillis, long localGmtOffset) {
+        if (startMillis == endMillis) {
+            return true;
+        }
+
+        // An event ending at midnight should still be a single-day event, so check
+        // time end-1.
+        int startDay = Time.getJulianDay(startMillis, localGmtOffset);
+        int endDay = Time.getJulianDay(endMillis - 1, localGmtOffset);
+        return startDay == endDay;
+    }
+
+    /**
+     * Returns TODAY or TOMORROW if applicable.  Otherwise returns NONE.
+     */
+    private static int isTodayOrTomorrow(Resources r, long dayMillis,
+            long currentMillis, long localGmtOffset) {
+        int startDay = Time.getJulianDay(dayMillis, localGmtOffset);
+        int currentDay = Time.getJulianDay(currentMillis, localGmtOffset);
+
+        int days = startDay - currentDay;
+        if (days == 1) {
+            return TOMORROW;
+        } else if (days == 0) {
+            return TODAY;
+        } else {
+            return NONE;
+        }
+    }
+
+    /**
+     * Formats a date or a time range according to the local conventions.
+     *
+     * This formats a date/time range using Calendar's time zone and the
+     * local conventions for the region of the device.
+     *
+     * If the {@link DateUtils#FORMAT_UTC} flag is used it will pass in
+     * the UTC time zone instead.
+     *
+     * @param context the context is required only if the time is shown
+     * @param startMillis the start time in UTC milliseconds
+     * @param endMillis the end time in UTC milliseconds
+     * @param flags a bit mask of options See
+     * {@link DateUtils#formatDateRange(Context, Formatter, long, long, int, String) formatDateRange}
+     * @return a string containing the formatted date/time range.
+     */
+    private static String formatDateRange(Context context, long startMillis,
+            long endMillis, int flags) {
+        String date;
+        String tz;
+        if ((flags & DateUtils.FORMAT_UTC) != 0) {
+            tz = Time.TIMEZONE_UTC;
+        } else {
+            tz = Time.getCurrentTimezone();
+        }
+        StringBuilder sb = new StringBuilder(50);
+        Formatter f = new Formatter(sb, Locale.getDefault());
+        sb.setLength(0);
+        date = DateUtils.formatDateRange(context, f, startMillis, endMillis, flags,
+                tz).toString();
+        return date;
+    }
+}
diff --git a/src/com/android/contacts/interactions/CalendarInteractionsLoader.java b/src/com/android/contacts/interactions/CalendarInteractionsLoader.java
new file mode 100644
index 0000000..6e25392
--- /dev/null
+++ b/src/com/android/contacts/interactions/CalendarInteractionsLoader.java
@@ -0,0 +1,232 @@
+package com.android.contacts.interactions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Loads a list of calendar interactions showing shared calendar events with everyone passed in
+ * {@param emailAddresses}.
+ *
+ * Note: the calendar provider treats mailing lists as atomic email addresses.
+ */
+public class CalendarInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
+    private static final String TAG = CalendarInteractionsLoader.class.getSimpleName();
+
+    private List<String> mEmailAddresses;
+    private int mMaxFutureToRetrieve;
+    private int mMaxPastToRetrieve;
+    private long mNumberFutureMillisecondToSearchLocalCalendar;
+    private long mNumberPastMillisecondToSearchLocalCalendar;
+    private List<ContactInteraction> mData;
+
+
+    /**
+     * @param maxFutureToRetrieve The maximum number of future events to retrieve
+     * @param maxPastToRetrieve The maximum number of past events to retrieve
+     */
+    public CalendarInteractionsLoader(Context context, List<String> emailAddresses,
+            int maxFutureToRetrieve, int maxPastToRetrieve,
+            long numberFutureMillisecondToSearchLocalCalendar,
+            long numberPastMillisecondToSearchLocalCalendar) {
+        super(context);
+        for (String address: emailAddresses) {
+            Log.v(TAG, address);
+        }
+        mEmailAddresses = emailAddresses;
+        mMaxFutureToRetrieve = maxFutureToRetrieve;
+        mMaxPastToRetrieve = maxPastToRetrieve;
+        mNumberFutureMillisecondToSearchLocalCalendar =
+                numberFutureMillisecondToSearchLocalCalendar;
+        mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar;
+    }
+
+    @Override
+    public List<ContactInteraction> loadInBackground() {
+        // Perform separate calendar queries for events in the past and future.
+        Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve);
+        Log.v(TAG, "future cursor.count() " + cursor.getCount());
+        List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor);
+        cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve);
+        Log.v(TAG, "past cursor.count() " + cursor.getCount());
+        List<ContactInteraction> interactions2 = getInteractionsFromEventsCursor(cursor);
+
+        ArrayList<ContactInteraction> allInteractions = new ArrayList<ContactInteraction>(
+                interactions.size() + interactions2.size());
+        allInteractions.addAll(interactions);
+        allInteractions.addAll(interactions2);
+
+        return allInteractions;
+    }
+
+    /**
+     * @return events inside phone owners' calendars, that are shared with people inside mEmails
+     */
+    private Cursor getSharedEventsCursor(boolean isFuture, int limit) {
+        List<String> calendarIds = getOwnedCalendarIds();
+        if (calendarIds == null) {
+            return null;
+        }
+        long timeMillis = System.currentTimeMillis();
+
+        List<String> selectionArgs = new ArrayList<>();
+        selectionArgs.addAll(mEmailAddresses);
+        selectionArgs.addAll(calendarIds);
+
+        // Add time constraints to selectionArgs
+        String timeOperator = isFuture ? " > " : " < ";
+        long pastTimeCutoff = timeMillis - mNumberPastMillisecondToSearchLocalCalendar;
+        long futureTimeCutoff = timeMillis
+                + mNumberFutureMillisecondToSearchLocalCalendar;
+        String[] timeArguments = {String.valueOf(timeMillis), String.valueOf(pastTimeCutoff),
+                String.valueOf(futureTimeCutoff)};
+        selectionArgs.addAll(Arrays.asList(timeArguments));
+
+        String orderBy = CalendarContract.Attendees.DTSTART + (isFuture ? " ASC " : " DESC ");
+        String selection = caseAndDotInsensitiveEmailComparisonClause(mEmailAddresses.size())
+                + " AND " + CalendarContract.Attendees.CALENDAR_ID
+                + " IN " + ContactInteractionUtil.questionMarks(calendarIds.size())
+                + " AND " + CalendarContract.Attendees.DTSTART + timeOperator + " ? "
+                + " AND " + CalendarContract.Attendees.DTSTART + " > ? "
+                + " AND " + CalendarContract.Attendees.DTSTART + " < ? ";
+
+        return getContext().getContentResolver().query(CalendarContract.Attendees.CONTENT_URI,
+                /* projection = */ null, selection,
+                selectionArgs.toArray(new String[selectionArgs.size()]),
+                orderBy + " LIMIT " + limit);
+    }
+
+    /**
+     * Returns a clause that checks whether an attendee's email is equal to one of
+     * {@param count} values. The comparison is insensitive to dots and case.
+     *
+     * NOTE #1: This function is only needed for supporting non google accounts. For calendars
+     * synced by a google account, attendee email values will be be modified by the server to ensure
+     * they match an entry in contacts.google.com.
+     *
+     * NOTE #2: This comparison clause can result in false positives. Ex#1, test@gmail.com will
+     * match test@gmailco.m. Ex#2, a.2@exchange.com will match a2@exchange.com (exchange addresses
+     * should be dot sensitive). This probably isn't a large concern.
+     */
+    private String caseAndDotInsensitiveEmailComparisonClause(int count) {
+        Preconditions.checkArgumentPositive(count, "Count needs to be positive");
+        final String COMPARISON
+                = " REPLACE(" + CalendarContract.Attendees.ATTENDEE_EMAIL
+                + ", '.', '') = REPLACE(?, '.', '') COLLATE NOCASE";
+        StringBuilder sb = new StringBuilder("( " + COMPARISON);
+        for (int i = 1; i < count; i++) {
+            sb.append(" OR " + COMPARISON);
+        }
+        return sb.append(")").toString();
+    }
+
+    /**
+     * @return A list with upto one Card. The Card contains events from {@param Cursor}.
+     * Only returns unique events.
+     */
+    private List<ContactInteraction> getInteractionsFromEventsCursor(Cursor cursor) {
+        try {
+            if (cursor == null || cursor.getCount() == 0) {
+                return Collections.emptyList();
+            }
+            Set<String> uniqueUris = new HashSet<String>();
+            ArrayList<ContactInteraction> interactions = new ArrayList<ContactInteraction>();
+            while (cursor.moveToNext()) {
+                ContentValues values = new ContentValues();
+                DatabaseUtils.cursorRowToContentValues(cursor, values);
+                CalendarInteraction calendarInteraction = new CalendarInteraction(values);
+                if (!uniqueUris.contains(calendarInteraction.getIntent().getData().toString())) {
+                    uniqueUris.add(calendarInteraction.getIntent().getData().toString());
+                    interactions.add(calendarInteraction);
+                }
+            }
+
+            return interactions;
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
+     * @return the Ids of calendars that are owned by accounts on the phone.
+     */
+    private List<String> getOwnedCalendarIds() {
+        String[] projection = new String[] {Calendars._ID, Calendars.CALENDAR_ACCESS_LEVEL};
+        Cursor cursor = getContext().getContentResolver().query(Calendars.CONTENT_URI, projection,
+                Calendars.VISIBLE + " = 1 AND " + Calendars.CALENDAR_ACCESS_LEVEL + " = ? ",
+                new String[] {String.valueOf(Calendars.CAL_ACCESS_OWNER)}, null);
+        try {
+            if (cursor == null || cursor.getCount() < 1) {
+                return null;
+            }
+            cursor.moveToPosition(-1);
+            List<String> calendarIds = new ArrayList<>(cursor.getCount());
+            while (cursor.moveToNext()) {
+                calendarIds.add(String.valueOf(cursor.getInt(0)));
+            }
+            return calendarIds;
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        super.onStartLoading();
+
+        if (mData != null) {
+            deliverResult(mData);
+        }
+
+        if (takeContentChanged() || mData == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        // Attempt to cancel the current load task if possible.
+        cancelLoad();
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped
+        onStopLoading();
+        if (mData != null) {
+            mData.clear();
+        }
+    }
+
+    @Override
+    public void deliverResult(List<ContactInteraction> data) {
+        mData = data;
+        if (isStarted()) {
+            super.deliverResult(data);
+        }
+    }
+}
diff --git a/src/com/android/contacts/interactions/ContactInteraction.java b/src/com/android/contacts/interactions/ContactInteraction.java
index a70a0a8..3f7a842 100644
--- a/src/com/android/contacts/interactions/ContactInteraction.java
+++ b/src/com/android/contacts/interactions/ContactInteraction.java
@@ -25,7 +25,6 @@
  */
 public interface ContactInteraction {
     Intent getIntent();
-    String getViewDate(Context context);
     long getInteractionDate();
     String getViewHeader(Context context);
     String getViewBody(Context context);
diff --git a/src/com/android/contacts/interactions/ContactInteractionUtil.java b/src/com/android/contacts/interactions/ContactInteractionUtil.java
index 453a5bd..a8a66f3 100644
--- a/src/com/android/contacts/interactions/ContactInteractionUtil.java
+++ b/src/com/android/contacts/interactions/ContactInteractionUtil.java
@@ -79,13 +79,13 @@
         // Turn compareCalendar to yesterday
         compareCalendar.add(Calendar.DAY_OF_YEAR, -1);
         if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
-            return context.getString(R.string.timestamp_string_yesterday);
+            return context.getString(R.string.yesterday);
         }
 
         // Turn compareCalendar to tomorrow
         compareCalendar.add(Calendar.DAY_OF_YEAR, 2);
         if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
-            return context.getString(R.string.timestamp_string_tomorrow);
+            return context.getString(R.string.tomorrow);
         }
         return DateUtils.formatDateTime(context, interactionCalendar.getTimeInMillis(),
                 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR);
diff --git a/src/com/android/contacts/interactions/SmsInteraction.java b/src/com/android/contacts/interactions/SmsInteraction.java
index e922056..c70356e 100644
--- a/src/com/android/contacts/interactions/SmsInteraction.java
+++ b/src/com/android/contacts/interactions/SmsInteraction.java
@@ -45,11 +45,6 @@
     }
 
     @Override
-    public String getViewDate(Context context) {
-        return ContactInteractionUtil.formatDateStringFromTimestamp(getDate(), context);
-    }
-
-    @Override
     public long getInteractionDate() {
         return getDate();
     }
@@ -66,7 +61,7 @@
 
     @Override
     public String getViewFooter(Context context) {
-        return getViewDate(context);
+        return ContactInteractionUtil.formatDateStringFromTimestamp(getDate(), context);
     }
 
     @Override
diff --git a/src/com/android/contacts/interactions/SmsInteractionsLoader.java b/src/com/android/contacts/interactions/SmsInteractionsLoader.java
index e0c8cf4..295c99a 100644
--- a/src/com/android/contacts/interactions/SmsInteractionsLoader.java
+++ b/src/com/android/contacts/interactions/SmsInteractionsLoader.java
@@ -72,16 +72,22 @@
 
         // Query the SMS database for the threads
         Cursor cursor = getSmsCursorFromThreads(threadIdStrings);
+        if (cursor != null) {
+            try {
+                List<ContactInteraction> interactions = new ArrayList<>();
+                while (cursor.moveToNext()) {
+                    ContentValues values = new ContentValues();
+                    DatabaseUtils.cursorRowToContentValues(cursor, values);
+                    interactions.add(new SmsInteraction(values));
+                }
 
-        List<ContactInteraction> interactions = new ArrayList<>();
-        while (cursor.moveToNext()) {
-            ContentValues values = new ContentValues();
-            DatabaseUtils.cursorRowToContentValues(cursor, values);
-            interactions.add(new SmsInteraction(values));
+                return interactions;
+            } finally {
+                cursor.close();
+            }
         }
 
-        Log.v(TAG, "end loadInBackground");
-        return interactions;
+        return Collections.emptyList();
     }
 
     /**
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 8b15deb..dcdeb1c 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -80,6 +80,8 @@
 import com.android.contacts.common.model.dataitem.PhoneDataItem;
 import com.android.contacts.common.util.DataStatus;
 import com.android.contacts.detail.ContactDetailDisplayUtils;
+import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.interactions.CalendarInteractionsLoader;
 import com.android.contacts.interactions.ContactDeletionInteraction;
 import com.android.contacts.interactions.ContactInteraction;
 import com.android.contacts.interactions.SmsInteractionsLoader;
@@ -93,6 +95,8 @@
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -192,8 +196,17 @@
     private static final String KEY_LOADER_EXTRA_SMS_PHONES =
             QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SMS_PHONES";
     private static final int MAX_SMS_RETRIEVE = 3;
+    private static final int LOADER_CALENDAR_ID = 2;
+    private static final String KEY_LOADER_EXTRA_CALENDAR_EMAILS =
+            QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_CALENDAR_EMAILS";
+    private static final int MAX_PAST_CALENDAR_RETRIEVE = 3;
+    private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3;
+    private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
+            180L * 24L * 60L * 60L * 1000L /* 180 days */;
+    private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR =
+            36L * 60L * 60L * 1000L /* 36 hours */;
 
-    private static final int[] mRecentLoaderIds = new int[LOADER_SMS_ID];
+    private static final int[] mRecentLoaderIds = new int[]{LOADER_SMS_ID, LOADER_CALENDAR_ID};
     private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
 
     private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
@@ -366,6 +379,9 @@
             // we need to restart the loader and reload the new contact.
             mContactLoader = (ContactLoader) getLoaderManager().restartLoader(
                     LOADER_CONTACT_ID, null, mLoaderContactCallbacks);
+            for (int interactionLoaderId : mRecentLoaderIds) {
+                getLoaderManager().destroyLoader(interactionLoaderId);
+            }
         }
     }
 
@@ -429,14 +445,17 @@
 
         final List<String> sortedActionMimeTypes = Lists.newArrayList();
         // Maintain a list of phone numbers to pass into SmsInteractionsLoader
-        final List<String> phoneNumbers = Lists.newArrayList();
+        final Set<String> phoneNumbers = new HashSet<>();
+        // Maintain a list of email addresses to pass into CalendarInteractionsLoader
+        final Set<String> emailAddresses = new HashSet<>();
         // List of Entry that makes up the ExpandingEntryCardView
         final List<Entry> entries = Lists.newArrayList();
 
         mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
-                computeEntriesAndActions(data, phoneNumbers, sortedActionMimeTypes, entries);
+                computeEntriesAndActions(data, phoneNumbers, emailAddresses,
+                        sortedActionMimeTypes, entries);
                 return null;
             }
 
@@ -447,7 +466,8 @@
                 // is still running before binding to UI. A new intent could invalidate
                 // the results, for example.
                 if (data == mContactData && !isCancelled()) {
-                    bindEntriesAndActions(entries, phoneNumbers, sortedActionMimeTypes);
+                    bindEntriesAndActions(entries, phoneNumbers, emailAddresses,
+                            sortedActionMimeTypes);
                     showActivity();
                 }
             }
@@ -456,21 +476,30 @@
     }
 
     private void bindEntriesAndActions(List<Entry> entries,
-            List<String> phoneNumbers,
+            Set<String> phoneNumbers,
+            Set<String> emailAddresses,
             List<String> sortedActionMimeTypes) {
         Trace.beginSection("start sms loader");
-
-        Bundle smsExtraBundle = new Bundle();
+        final Bundle smsExtraBundle = new Bundle();
         smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES,
                 phoneNumbers.toArray(new String[phoneNumbers.size()]));
         getLoaderManager().initLoader(
                 LOADER_SMS_ID,
                 smsExtraBundle,
                 mLoaderInteractionsCallbacks);
-
         Trace.endSection();
-        Trace.beginSection("bind communicate card");
 
+        Trace.beginSection("start calendar loader");
+        final Bundle calendarExtraBundle = new Bundle();
+        calendarExtraBundle.putStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS,
+                emailAddresses.toArray(new String[emailAddresses.size()]));
+        getLoaderManager().initLoader(
+                LOADER_CALENDAR_ID,
+                calendarExtraBundle,
+                mLoaderInteractionsCallbacks);
+        Trace.endSection();
+
+        Trace.beginSection("bind communicate card");
         if (entries.size() > 0) {
             mCommunicationCard.initialize(entries,
                     /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
@@ -497,8 +526,8 @@
         }
     }
 
-    private void computeEntriesAndActions(Contact data, List<String> phoneNumbers,
-            List<String> sortedActionMimeTypes, List<Entry> entries) {
+    private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers,
+            Set<String> emailAddresses, List<String> sortedActionMimeTypes, List<Entry> entries) {
         Trace.beginSection("inflate entries and actions");
 
         final ResolveCache cache = ResolveCache.getInstance(this);
@@ -513,6 +542,10 @@
                     phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber());
                 }
 
+                if (dataItem instanceof EmailDataItem) {
+                    emailAddresses.add(((EmailDataItem) dataItem).getAddress());
+                }
+
                 // Skip this data item if MIME-type excluded
                 if (isMimeExcluded(mimeType)) continue;
 
@@ -854,6 +887,16 @@
                             args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES),
                             MAX_SMS_RETRIEVE);
                     break;
+                case LOADER_CALENDAR_ID:
+                    Log.v(TAG, "LOADER_CALENDAR_ID");
+                    loader = new CalendarInteractionsLoader(
+                            QuickContactActivity.this,
+                            Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS)),
+                            MAX_FUTURE_CALENDAR_RETRIEVE,
+                            MAX_PAST_CALENDAR_RETRIEVE,
+                            FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR,
+                            PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR);
+                    break;
             }
             return loader;
         }
@@ -864,6 +907,8 @@
             if (mRecentLoaderResults == null) {
                 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>();
             }
+            Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " +
+                    data.size());
             mRecentLoaderResults.put(loader.getId(), data);
 
             if (isAllRecentDataLoaded()) {
@@ -929,7 +974,7 @@
         } else {
             mDrawablesToTint.add(drawable);
         }
-        return drawable; 
+        return drawable;
     }
 
     /**
diff --git a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
index 05ad9b5..4802b46 100644
--- a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
+++ b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
@@ -83,7 +83,7 @@
     public void testFormatDateStringFromTimestamp_yesterday() {
         // Test yesterday and tomorrow (Yesterday or Tomorrow shown)
         calendar.add(Calendar.DAY_OF_YEAR, -1);
-        assertEquals(getContext().getResources().getString(R.string.timestamp_string_yesterday),
+        assertEquals(getContext().getResources().getString(R.string.yesterday),
                 ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
                         getContext()));
     }
@@ -95,14 +95,14 @@
         long lastYear = calendar.getTimeInMillis();
         calendar.add(Calendar.DAY_OF_YEAR, 1);
 
-        assertEquals(getContext().getResources().getString(R.string.timestamp_string_yesterday),
+        assertEquals(getContext().getResources().getString(R.string.yesterday),
                 ContactInteractionUtil.formatDateStringFromTimestamp(lastYear,
                         getContext(), calendar));
     }
 
     public void testFormatDateStringFromTimestamp_tomorrow() {
         calendar.add(Calendar.DAY_OF_YEAR, 1);
-        assertEquals(getContext().getResources().getString(R.string.timestamp_string_tomorrow),
+        assertEquals(getContext().getResources().getString(R.string.tomorrow),
                 ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
                         getContext()));
     }
@@ -112,7 +112,7 @@
         long thisYear = calendar.getTimeInMillis();
         calendar.add(Calendar.DAY_OF_YEAR, -1);
 
-        assertEquals(getContext().getResources().getString(R.string.timestamp_string_tomorrow),
+        assertEquals(getContext().getResources().getString(R.string.tomorrow),
                 ContactInteractionUtil.formatDateStringFromTimestamp(thisYear,
                         getContext(), calendar));
     }