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      */
CalendarInteractionsLoader(Context context, List<String> emailAddresses, int maxFutureToRetrieve, int maxPastToRetrieve, long numberFutureMillisecondToSearchLocalCalendar, long numberPastMillisecondToSearchLocalCalendar)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
loadInBackground()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      */
getSharedEventsCursor(boolean isFuture, int limit)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@gmail.com will
127      * match test@gmailco.m. Ex#2, a.2@exchange.com will match a2@exchange.com (exchange addresses
128      * should be dot sensitive). This probably isn't a large concern.
129      */
caseAndDotInsensitiveEmailComparisonClause(int count)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      */
getInteractionsFromEventsCursor(Cursor cursor)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      */
getOwnedCalendarIds()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
onStartLoading()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
onStopLoading()210     protected void onStopLoading() {
211         // Attempt to cancel the current load task if possible.
212         cancelLoad();
213     }
214 
215     @Override
onReset()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
deliverResult(List<ContactInteraction> data)227     public void deliverResult(List<ContactInteraction> data) {
228         mData = data;
229         if (isStarted()) {
230             super.deliverResult(data);
231         }
232     }
233 }
234