blob: bc102e5b06fedcd4b8bd72550c4efc82edc84834 [file] [log] [blame]
Paul Soulos899aa212014-06-11 12:04:43 -07001package com.android.contacts.interactions;
2
Brian Attwell0b89e302015-06-23 23:23:19 -07003import android.Manifest.permission;
Paul Soulos899aa212014-06-11 12:04:43 -07004import android.content.AsyncTaskLoader;
Paul Soulos899aa212014-06-11 12:04:43 -07005import android.content.ContentValues;
6import android.content.Context;
7import android.database.Cursor;
8import android.database.DatabaseUtils;
9import android.provider.CalendarContract;
Paul Soulos899aa212014-06-11 12:04:43 -070010import android.provider.CalendarContract.Calendars;
Paul Soulos899aa212014-06-11 12:04:43 -070011import android.util.Log;
12
Gary Mai0a49afa2016-12-05 15:53:58 -080013import com.android.contacts.util.PermissionsUtil;
14
15import com.google.common.base.Preconditions;
16
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collections;
20import java.util.HashSet;
21import java.util.List;
22import java.util.Set;
23
Paul Soulos899aa212014-06-11 12:04:43 -070024
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 */
31public 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 */
46 public CalendarInteractionsLoader(Context context, List<String> emailAddresses,
47 int maxFutureToRetrieve, int maxPastToRetrieve,
48 long numberFutureMillisecondToSearchLocalCalendar,
49 long numberPastMillisecondToSearchLocalCalendar) {
50 super(context);
Paul Soulos899aa212014-06-11 12:04:43 -070051 mEmailAddresses = emailAddresses;
52 mMaxFutureToRetrieve = maxFutureToRetrieve;
53 mMaxPastToRetrieve = maxPastToRetrieve;
54 mNumberFutureMillisecondToSearchLocalCalendar =
55 numberFutureMillisecondToSearchLocalCalendar;
56 mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar;
57 }
58
59 @Override
60 public List<ContactInteraction> loadInBackground() {
Brian Attwell0b89e302015-06-23 23:23:19 -070061 if (!PermissionsUtil.hasPermission(getContext(), permission.READ_CALENDAR)
62 || mEmailAddresses == null || mEmailAddresses.size() < 1) {
Paul Soulos84ef8a42014-06-11 15:05:07 -070063 return Collections.emptyList();
64 }
Paul Soulos899aa212014-06-11 12:04:43 -070065 // Perform separate calendar queries for events in the past and future.
66 Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve);
Paul Soulos899aa212014-06-11 12:04:43 -070067 List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor);
68 cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve);
Paul Soulos899aa212014-06-11 12:04:43 -070069 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
Paul Soulos0a76a462014-06-19 10:20:45 -070076 Log.v(TAG, "# ContactInteraction Loaded: " + allInteractions.size());
Paul Soulos899aa212014-06-11 12:04:43 -070077 return allInteractions;
78 }
79
80 /**
81 * @return events inside phone owners' calendars, that are shared with people inside mEmails
82 */
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
Brian Attwell6446d832014-10-20 13:48:03 -0700103 // 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
Paul Soulos899aa212014-06-11 12:04:43 -0700107 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 + " > ? "
Brian Attwell6446d832014-10-20 13:48:03 -0700113 + " AND " + CalendarContract.Attendees.DTSTART + " < ? "
114 + " AND " + IS_NOT_TEMPORARY_COPY_OF_LOCAL_EVENT;
Paul Soulos899aa212014-06-11 12:04:43 -0700115
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 */
134 private String caseAndDotInsensitiveEmailComparisonClause(int count) {
Brian Attwell3bb467d2014-06-16 17:47:40 -0700135 Preconditions.checkArgument(count > 0, "Count needs to be positive");
Paul Soulos899aa212014-06-11 12:04:43 -0700136 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 */
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 */
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
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
214 protected void onStopLoading() {
215 // Attempt to cancel the current load task if possible.
216 cancelLoad();
217 }
218
219 @Override
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
231 public void deliverResult(List<ContactInteraction> data) {
232 mData = data;
233 if (isStarted()) {
234 super.deliverResult(data);
235 }
236 }
237}