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