summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml1
-rw-r--r--res/drawable-hdpi/ic_event_24dp.pngbin0 -> 260 bytes
-rw-r--r--res/drawable-mdpi/ic_event_24dp.pngbin0 -> 205 bytes
-rw-r--r--res/drawable-xhdpi/ic_event_24dp.pngbin0 -> 264 bytes
-rw-r--r--res/drawable-xxhdpi/ic_event_24dp.pngbin0 -> 364 bytes
-rw-r--r--res/values/strings.xml21
-rw-r--r--src/com/android/contacts/interactions/CalendarInteraction.java269
-rw-r--r--src/com/android/contacts/interactions/CalendarInteractionUtils.java192
-rw-r--r--src/com/android/contacts/interactions/CalendarInteractionsLoader.java232
-rw-r--r--src/com/android/contacts/interactions/ContactInteraction.java1
-rw-r--r--src/com/android/contacts/interactions/ContactInteractionUtil.java4
-rw-r--r--src/com/android/contacts/interactions/SmsInteraction.java7
-rw-r--r--src/com/android/contacts/interactions/SmsInteractionsLoader.java22
-rw-r--r--src/com/android/contacts/quickcontact/QuickContactActivity.java69
-rw-r--r--tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java8
15 files changed, 788 insertions, 38 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e2aad858c..61010fabf 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 000000000..023695a5c
--- /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 000000000..f5abeb718
--- /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 000000000..a2bd4b216
--- /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 000000000..f27a42491
--- /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 c1bd3c0c1..696ea08b0 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 000000000..68e37f797
--- /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 000000000..c7943f0b7
--- /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 000000000..6e2539233
--- /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 a70a0a8af..3f7a84204 100644
--- a/src/com/android/contacts/interactions/ContactInteraction.java
+++ b/src/com/android/contacts/interactions/ContactInteraction.java
@@ -25,7 +25,6 @@ import android.net.Uri;
*/
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 453a5bdd5..a8a66f387 100644
--- a/src/com/android/contacts/interactions/ContactInteractionUtil.java
+++ b/src/com/android/contacts/interactions/ContactInteractionUtil.java
@@ -79,13 +79,13 @@ public class ContactInteractionUtil {
// 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 e922056f1..c70356ec4 100644
--- a/src/com/android/contacts/interactions/SmsInteraction.java
+++ b/src/com/android/contacts/interactions/SmsInteraction.java
@@ -45,11 +45,6 @@ public class SmsInteraction implements ContactInteraction {
}
@Override
- public String getViewDate(Context context) {
- return ContactInteractionUtil.formatDateStringFromTimestamp(getDate(), context);
- }
-
- @Override
public long getInteractionDate() {
return getDate();
}
@@ -66,7 +61,7 @@ public class SmsInteraction implements ContactInteraction {
@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 e0c8cf4fb..295c99a46 100644
--- a/src/com/android/contacts/interactions/SmsInteractionsLoader.java
+++ b/src/com/android/contacts/interactions/SmsInteractionsLoader.java
@@ -72,16 +72,22 @@ public class SmsInteractionsLoader extends AsyncTaskLoader<List<ContactInteracti
// Query the SMS database for the threads
Cursor cursor = getSmsCursorFromThreads(threadIdStrings);
-
- List<ContactInteraction> interactions = new ArrayList<>();
- while (cursor.moveToNext()) {
- ContentValues values = new ContentValues();
- DatabaseUtils.cursorRowToContentValues(cursor, values);
- interactions.add(new SmsInteraction(values));
+ 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));
+ }
+
+ 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 8b15debca..dcdeb1c01 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.ImDataItem;
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.base.Preconditions;
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 @@ public class QuickContactActivity extends ContactsActivity {
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[] mRecentLoaderIds = new int[LOADER_SMS_ID];
+ 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, LOADER_CALENDAR_ID};
private Map<Integer, List<ContactInteraction>> mRecentLoaderResults;
private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment";
@@ -366,6 +379,9 @@ public class QuickContactActivity extends ContactsActivity {
// 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 @@ public class QuickContactActivity extends ContactsActivity {
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 @@ public class QuickContactActivity extends ContactsActivity {
// 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 @@ public class QuickContactActivity extends ContactsActivity {
}
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("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");
+ Trace.beginSection("bind communicate card");
if (entries.size() > 0) {
mCommunicationCard.initialize(entries,
/* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
@@ -497,8 +526,8 @@ public class QuickContactActivity extends ContactsActivity {
}
}
- 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 @@ public class QuickContactActivity extends ContactsActivity {
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 @@ public class QuickContactActivity extends ContactsActivity {
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 @@ public class QuickContactActivity extends ContactsActivity {
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 @@ public class QuickContactActivity extends ContactsActivity {
} 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 05ad9b594..4802b46e6 100644
--- a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
+++ b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
@@ -83,7 +83,7 @@ public class ContactInteractionUtilTest extends AndroidTestCase {
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 @@ public class ContactInteractionUtilTest extends AndroidTestCase {
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 @@ public class ContactInteractionUtilTest extends AndroidTestCase {
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));
}