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