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