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