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