Home | History | Annotate | Download | only in interactions
      1 package com.android.contacts.interactions;
      2 
      3 import com.google.common.base.Preconditions;
      4 
      5 import java.util.ArrayList;
      6 import java.util.Arrays;
      7 import java.util.Collections;
      8 import java.util.HashSet;
      9 import java.util.List;
     10 import java.util.Set;
     11 
     12 import android.content.AsyncTaskLoader;
     13 import android.content.ContentValues;
     14 import android.content.Context;
     15 import android.database.Cursor;
     16 import android.database.DatabaseUtils;
     17 import android.provider.CalendarContract;
     18 import android.provider.CalendarContract.Calendars;
     19 import android.util.Log;
     20 
     21 
     22 /**
     23  * Loads a list of calendar interactions showing shared calendar events with everyone passed in
     24  * {@param emailAddresses}.
     25  *
     26  * Note: the calendar provider treats mailing lists as atomic email addresses.
     27  */
     28 public class CalendarInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
     29     private static final String TAG = CalendarInteractionsLoader.class.getSimpleName();
     30 
     31     private List<String> mEmailAddresses;
     32     private int mMaxFutureToRetrieve;
     33     private int mMaxPastToRetrieve;
     34     private long mNumberFutureMillisecondToSearchLocalCalendar;
     35     private long mNumberPastMillisecondToSearchLocalCalendar;
     36     private List<ContactInteraction> mData;
     37 
     38 
     39     /**
     40      * @param maxFutureToRetrieve The maximum number of future events to retrieve
     41      * @param maxPastToRetrieve The maximum number of past events to retrieve
     42      */
     43     public CalendarInteractionsLoader(Context context, List<String> emailAddresses,
     44             int maxFutureToRetrieve, int maxPastToRetrieve,
     45             long numberFutureMillisecondToSearchLocalCalendar,
     46             long numberPastMillisecondToSearchLocalCalendar) {
     47         super(context);
     48         mEmailAddresses = emailAddresses;
     49         mMaxFutureToRetrieve = maxFutureToRetrieve;
     50         mMaxPastToRetrieve = maxPastToRetrieve;
     51         mNumberFutureMillisecondToSearchLocalCalendar =
     52                 numberFutureMillisecondToSearchLocalCalendar;
     53         mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar;
     54     }
     55 
     56     @Override
     57     public List<ContactInteraction> loadInBackground() {
     58         if (mEmailAddresses == null || mEmailAddresses.size() < 1) {
     59             return Collections.emptyList();
     60         }
     61         // Perform separate calendar queries for events in the past and future.
     62         Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve);
     63         List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor);
     64         cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve);
     65         List<ContactInteraction> interactions2 = getInteractionsFromEventsCursor(cursor);
     66 
     67         ArrayList<ContactInteraction> allInteractions = new ArrayList<ContactInteraction>(
     68                 interactions.size() + interactions2.size());
     69         allInteractions.addAll(interactions);
     70         allInteractions.addAll(interactions2);
     71 
     72         Log.v(TAG, "# ContactInteraction Loaded: " + allInteractions.size());
     73         return allInteractions;
     74     }
     75 
     76     /**
     77      * @return events inside phone owners' calendars, that are shared with people inside mEmails
     78      */
     79     private Cursor getSharedEventsCursor(boolean isFuture, int limit) {
     80         List<String> calendarIds = getOwnedCalendarIds();
     81         if (calendarIds == null) {
     82             return null;
     83         }
     84         long timeMillis = System.currentTimeMillis();
     85 
     86         List<String> selectionArgs = new ArrayList<>();
     87         selectionArgs.addAll(mEmailAddresses);
     88         selectionArgs.addAll(calendarIds);
     89 
     90         // Add time constraints to selectionArgs
     91         String timeOperator = isFuture ? " > " : " < ";
     92         long pastTimeCutoff = timeMillis - mNumberPastMillisecondToSearchLocalCalendar;
     93         long futureTimeCutoff = timeMillis
     94                 + mNumberFutureMillisecondToSearchLocalCalendar;
     95         String[] timeArguments = {String.valueOf(timeMillis), String.valueOf(pastTimeCutoff),
     96                 String.valueOf(futureTimeCutoff)};
     97         selectionArgs.addAll(Arrays.asList(timeArguments));
     98 
     99         // When LAST_SYNCED = 1, the event is not a real event. We should ignore all such events.
    100         String IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT
    101                 = CalendarContract.Attendees.LAST_SYNCED + " = 0";
    102 
    103         String orderBy = CalendarContract.Attendees.DTSTART + (isFuture ? " ASC " : " DESC ");
    104         String selection = caseAndDotInsensitiveEmailComparisonClause(mEmailAddresses.size())
    105                 + " AND " + CalendarContract.Attendees.CALENDAR_ID
    106                 + " IN " + ContactInteractionUtil.questionMarks(calendarIds.size())
    107                 + " AND " + CalendarContract.Attendees.DTSTART + timeOperator + " ? "
    108                 + " AND " + CalendarContract.Attendees.DTSTART + " > ? "
    109                 + " AND " + CalendarContract.Attendees.DTSTART + " < ? "
    110                 + " AND " + IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT;
    111 
    112         return getContext().getContentResolver().query(CalendarContract.Attendees.CONTENT_URI,
    113                 /* projection = */ null, selection,
    114                 selectionArgs.toArray(new String[selectionArgs.size()]),
    115                 orderBy + " LIMIT " + limit);
    116     }
    117 
    118     /**
    119      * Returns a clause that checks whether an attendee's email is equal to one of
    120      * {@param count} values. The comparison is insensitive to dots and case.
    121      *
    122      * NOTE #1: This function is only needed for supporting non google accounts. For calendars
    123      * synced by a google account, attendee email values will be be modified by the server to ensure
    124      * they match an entry in contacts.google.com.
    125      *
    126      * NOTE #2: This comparison clause can result in false positives. Ex#1, test (at) gmail.com will
    127      * match test (at) gmailco.m. Ex#2, a.2 (at) exchange.com will match a2 (at) exchange.com (exchange addresses
    128      * should be dot sensitive). This probably isn't a large concern.
    129      */
    130     private String caseAndDotInsensitiveEmailComparisonClause(int count) {
    131         Preconditions.checkArgument(count > 0, "Count needs to be positive");
    132         final String COMPARISON
    133                 = " REPLACE(" + CalendarContract.Attendees.ATTENDEE_EMAIL
    134                 + ", '.', '') = REPLACE(?, '.', '') COLLATE NOCASE";
    135         StringBuilder sb = new StringBuilder("( " + COMPARISON);
    136         for (int i = 1; i < count; i++) {
    137             sb.append(" OR " + COMPARISON);
    138         }
    139         return sb.append(")").toString();
    140     }
    141 
    142     /**
    143      * @return A list with upto one Card. The Card contains events from {@param Cursor}.
    144      * Only returns unique events.
    145      */
    146     private List<ContactInteraction> getInteractionsFromEventsCursor(Cursor cursor) {
    147         try {
    148             if (cursor == null || cursor.getCount() == 0) {
    149                 return Collections.emptyList();
    150             }
    151             Set<String> uniqueUris = new HashSet<String>();
    152             ArrayList<ContactInteraction> interactions = new ArrayList<ContactInteraction>();
    153             while (cursor.moveToNext()) {
    154                 ContentValues values = new ContentValues();
    155                 DatabaseUtils.cursorRowToContentValues(cursor, values);
    156                 CalendarInteraction calendarInteraction = new CalendarInteraction(values);
    157                 if (!uniqueUris.contains(calendarInteraction.getIntent().getData().toString())) {
    158                     uniqueUris.add(calendarInteraction.getIntent().getData().toString());
    159                     interactions.add(calendarInteraction);
    160                 }
    161             }
    162 
    163             return interactions;
    164         } finally {
    165             if (cursor != null) {
    166                 cursor.close();
    167             }
    168         }
    169     }
    170 
    171     /**
    172      * @return the Ids of calendars that are owned by accounts on the phone.
    173      */
    174     private List<String> getOwnedCalendarIds() {
    175         String[] projection = new String[] {Calendars._ID, Calendars.CALENDAR_ACCESS_LEVEL};
    176         Cursor cursor = getContext().getContentResolver().query(Calendars.CONTENT_URI, projection,
    177                 Calendars.VISIBLE + " = 1 AND " + Calendars.CALENDAR_ACCESS_LEVEL + " = ? ",
    178                 new String[] {String.valueOf(Calendars.CAL_ACCESS_OWNER)}, null);
    179         try {
    180             if (cursor == null || cursor.getCount() < 1) {
    181                 return null;
    182             }
    183             cursor.moveToPosition(-1);
    184             List<String> calendarIds = new ArrayList<>(cursor.getCount());
    185             while (cursor.moveToNext()) {
    186                 calendarIds.add(String.valueOf(cursor.getInt(0)));
    187             }
    188             return calendarIds;
    189         } finally {
    190             if (cursor != null) {
    191                 cursor.close();
    192             }
    193         }
    194     }
    195 
    196     @Override
    197     protected void onStartLoading() {
    198         super.onStartLoading();
    199 
    200         if (mData != null) {
    201             deliverResult(mData);
    202         }
    203 
    204         if (takeContentChanged() || mData == null) {
    205             forceLoad();
    206         }
    207     }
    208 
    209     @Override
    210     protected void onStopLoading() {
    211         // Attempt to cancel the current load task if possible.
    212         cancelLoad();
    213     }
    214 
    215     @Override
    216     protected void onReset() {
    217         super.onReset();
    218 
    219         // Ensure the loader is stopped
    220         onStopLoading();
    221         if (mData != null) {
    222             mData.clear();
    223         }
    224     }
    225 
    226     @Override
    227     public void deliverResult(List<ContactInteraction> data) {
    228         mData = data;
    229         if (isStarted()) {
    230             super.deliverResult(data);
    231         }
    232     }
    233 }
    234