CalendarInteractionsLoader.java revision 6446d83a0041ca56a23a43f74ab3ebc1d3ab8e82
1package com.android.contacts.interactions; 2 3import com.google.common.base.Preconditions; 4 5import 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; 13import android.content.ContentValues; 14import android.content.Context; 15import android.database.Cursor; 16import android.database.DatabaseUtils; 17import android.provider.CalendarContract; 18import android.provider.CalendarContract.Calendars; 19import 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 */ 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 mEmailAddresses = emailAddresses; 49 mMaxFutureToRetrieve = maxFutureToRetrieve; 50 mMaxPastToRetrieve = maxPastToRetrieve; 51 mNumberFutureMillisecondToSearchLocalCalendar = 52 numberFutureMillisecondToSearchLocalCalendar; 53 mNumberPastMillisecondToSearchLocalCalendar = numberPastMillisecondToSearchLocalCalendar; 54 } 55 56 @Override 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 */ 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 */ 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 */ 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 */ 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 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 210 protected void onStopLoading() { 211 // Attempt to cancel the current load task if possible. 212 cancelLoad(); 213 } 214 215 @Override 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 227 public void deliverResult(List<ContactInteraction> data) { 228 mData = data; 229 if (isStarted()) { 230 super.deliverResult(data); 231 } 232 } 233} 234