diff options
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 Binary files differnew file mode 100644 index 000000000..023695a5c --- /dev/null +++ b/res/drawable-hdpi/ic_event_24dp.png diff --git a/res/drawable-mdpi/ic_event_24dp.png b/res/drawable-mdpi/ic_event_24dp.png Binary files differnew file mode 100644 index 000000000..f5abeb718 --- /dev/null +++ b/res/drawable-mdpi/ic_event_24dp.png diff --git a/res/drawable-xhdpi/ic_event_24dp.png b/res/drawable-xhdpi/ic_event_24dp.png Binary files differnew file mode 100644 index 000000000..a2bd4b216 --- /dev/null +++ b/res/drawable-xhdpi/ic_event_24dp.png diff --git a/res/drawable-xxhdpi/ic_event_24dp.png b/res/drawable-xxhdpi/ic_event_24dp.png Binary files differnew file mode 100644 index 000000000..f27a42491 --- /dev/null +++ b/res/drawable-xxhdpi/ic_event_24dp.png 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)); } |