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