• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
CalendarInteractionsLoader(Context context, List<String> emailAddresses, int maxFutureToRetrieve, int maxPastToRetrieve, long numberFutureMillisecondToSearchLocalCalendar, long numberPastMillisecondToSearchLocalCalendar)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
loadInBackground()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      */
getSharedEventsCursor(boolean isFuture, int limit)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@gmail.com will
133      * match test@gmailco.m. Ex#2, a.2@exchange.com will match a2@exchange.com (exchange addresses
134      * should be dot sensitive). This probably isn't a large concern.
135      */
caseAndDotInsensitiveEmailComparisonClause(int count)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      */
getInteractionsFromEventsCursor(Cursor cursor)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      */
getOwnedCalendarIds()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
onStartLoading()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
onStopLoading()216     protected void onStopLoading() {
217         // Attempt to cancel the current load task if possible.
218         cancelLoad();
219     }
220 
221     @Override
onReset()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
deliverResult(List<ContactInteraction> data)233     public void deliverResult(List<ContactInteraction> data) {
234         mData = data;
235         if (isStarted()) {
236             super.deliverResult(data);
237         }
238     }
239 }
240