| package com.android.contacts.interactions; |
| |
| import com.google.common.base.Preconditions; |
| |
| import com.android.contacts.common.util.PermissionsUtil; |
| |
| 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.Manifest.permission; |
| import android.content.AsyncTaskLoader; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.DatabaseUtils; |
| import android.provider.CalendarContract; |
| import android.provider.CalendarContract.Calendars; |
| import android.util.Log; |
| |
| |
| /** |
| * 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); |
| mEmailAddresses = emailAddresses; |
| mMaxFutureToRetrieve = maxFutureToRetrieve; |
| mMaxPastToRetrieve = maxPastToRetrieve; |
| mNumberFutureMillisecondToSearchLocalCalendar = |
| numberFutureMillisecondToSearchLocalCalendar; |
| mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar; |
| } |
| |
| @Override |
| public List<ContactInteraction> loadInBackground() { |
| if (!PermissionsUtil.hasPermission(getContext(), permission.READ_CALENDAR) |
| || mEmailAddresses == null || mEmailAddresses.size() < 1) { |
| return Collections.emptyList(); |
| } |
| // Perform separate calendar queries for events in the past and future. |
| Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve); |
| List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor); |
| cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve); |
| List<ContactInteraction> interactions2 = getInteractionsFromEventsCursor(cursor); |
| |
| ArrayList<ContactInteraction> allInteractions = new ArrayList<ContactInteraction>( |
| interactions.size() + interactions2.size()); |
| allInteractions.addAll(interactions); |
| allInteractions.addAll(interactions2); |
| |
| Log.v(TAG, "# ContactInteraction Loaded: " + allInteractions.size()); |
| 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)); |
| |
| // When LAST_SYNCED = 1, the event is not a real event. We should ignore all such events. |
| String IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT |
| = CalendarContract.Attendees.LAST_SYNCED + " = 0"; |
| |
| 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 + " < ? " |
| + " AND " + IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT; |
| |
| 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.checkArgument(count > 0, "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); |
| } |
| } |
| } |