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