1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.calllog;
18
19import com.android.common.io.MoreCloseables;
20import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
21import com.google.android.collect.Lists;
22
23import android.content.AsyncQueryHandler;
24import android.content.ContentResolver;
25import android.content.ContentValues;
26import android.database.Cursor;
27import android.database.MatrixCursor;
28import android.database.MergeCursor;
29import android.database.sqlite.SQLiteDatabaseCorruptException;
30import android.database.sqlite.SQLiteDiskIOException;
31import android.database.sqlite.SQLiteException;
32import android.database.sqlite.SQLiteFullException;
33import android.os.Handler;
34import android.os.Looper;
35import android.os.Message;
36import android.provider.CallLog.Calls;
37import android.provider.VoicemailContract.Status;
38import android.util.Log;
39
40import java.lang.ref.WeakReference;
41import java.util.List;
42import java.util.concurrent.TimeUnit;
43
44import javax.annotation.concurrent.GuardedBy;
45
46/** Handles asynchronous queries to the call log. */
47/*package*/ class CallLogQueryHandler extends AsyncQueryHandler {
48    private static final String[] EMPTY_STRING_ARRAY = new String[0];
49
50    private static final String TAG = "CallLogQueryHandler";
51
52    /** The token for the query to fetch the new entries from the call log. */
53    private static final int QUERY_NEW_CALLS_TOKEN = 53;
54    /** The token for the query to fetch the old entries from the call log. */
55    private static final int QUERY_OLD_CALLS_TOKEN = 54;
56    /** The token for the query to mark all missed calls as old after seeing the call log. */
57    private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
58    /** The token for the query to mark all new voicemails as old. */
59    private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
60    /** The token for the query to mark all missed calls as read after seeing the call log. */
61    private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
62    /** The token for the query to fetch voicemail status messages. */
63    private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
64
65    /**
66     * The time window from the current time within which an unread entry will be added to the new
67     * section.
68     */
69    private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7);
70
71    private final WeakReference<Listener> mListener;
72
73    /** The cursor containing the new calls, or null if they have not yet been fetched. */
74    @GuardedBy("this") private Cursor mNewCallsCursor;
75    /** The cursor containing the old calls, or null if they have not yet been fetched. */
76    @GuardedBy("this") private Cursor mOldCallsCursor;
77    /**
78     * The identifier of the latest calls request.
79     * <p>
80     * A request for the list of calls requires two queries and hence the two cursor
81     * {@link #mNewCallsCursor} and {@link #mOldCallsCursor} above, corresponding to
82     * {@link #QUERY_NEW_CALLS_TOKEN} and {@link #QUERY_OLD_CALLS_TOKEN}.
83     * <p>
84     * When a new request is about to be started, existing cursors are closed. However, it is
85     * possible that one of the queries completes after the new request has started. This means that
86     * we might merge two cursors that do not correspond to the same request. Moreover, this may
87     * lead to a resource leak if the same query completes and we override the cursor without
88     * closing it first.
89     * <p>
90     * To make sure we only join two cursors from the same request, we use this variable to store
91     * the request id of the latest request and make sure we only process cursors corresponding to
92     * the this request.
93     */
94    @GuardedBy("this") private int mCallsRequestId;
95
96    /**
97     * Simple handler that wraps background calls to catch
98     * {@link SQLiteException}, such as when the disk is full.
99     */
100    protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
101        public CatchingWorkerHandler(Looper looper) {
102            super(looper);
103        }
104
105        @Override
106        public void handleMessage(Message msg) {
107            try {
108                // Perform same query while catching any exceptions
109                super.handleMessage(msg);
110            } catch (SQLiteDiskIOException e) {
111                Log.w(TAG, "Exception on background worker thread", e);
112            } catch (SQLiteFullException e) {
113                Log.w(TAG, "Exception on background worker thread", e);
114            } catch (SQLiteDatabaseCorruptException e) {
115                Log.w(TAG, "Exception on background worker thread", e);
116            }
117        }
118    }
119
120    @Override
121    protected Handler createHandler(Looper looper) {
122        // Provide our special handler that catches exceptions
123        return new CatchingWorkerHandler(looper);
124    }
125
126    public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
127        super(contentResolver);
128        mListener = new WeakReference<Listener>(listener);
129    }
130
131    /** Creates a cursor that contains a single row and maps the section to the given value. */
132    private Cursor createHeaderCursorFor(int section) {
133        MatrixCursor matrixCursor =
134                new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
135        // The values in this row correspond to default values for _PROJECTION from CallLogQuery
136        // plus the section value.
137        matrixCursor.addRow(new Object[]{
138                0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0,
139                section
140        });
141        return matrixCursor;
142    }
143
144    /** Returns a cursor for the old calls header. */
145    private Cursor createOldCallsHeaderCursor() {
146        return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
147    }
148
149    /** Returns a cursor for the new calls header. */
150    private Cursor createNewCallsHeaderCursor() {
151        return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER);
152    }
153
154    /**
155     * Fetches the list of calls from the call log.
156     * <p>
157     * It will asynchronously update the content of the list view when the fetch completes.
158     */
159    public void fetchAllCalls() {
160        cancelFetch();
161        int requestId = newCallsRequest();
162        fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, false /*voicemailOnly*/);
163        fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, false /*voicemailOnly*/);
164    }
165
166    /**
167     * Fetches the list of calls from the call log but include only the voicemail.
168     * <p>
169     * It will asynchronously update the content of the list view when the fetch completes.
170     */
171    public void fetchVoicemailOnly() {
172        cancelFetch();
173        int requestId = newCallsRequest();
174        fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, true /*voicemailOnly*/);
175        fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, true /*voicemailOnly*/);
176    }
177
178
179    public void fetchVoicemailStatus() {
180        startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
181                VoicemailStatusHelperImpl.PROJECTION, null, null, null);
182    }
183
184    /** Fetches the list of calls in the call log, either the new one or the old ones. */
185    private void fetchCalls(int token, int requestId, boolean isNew, boolean voicemailOnly) {
186        // We need to check for NULL explicitly otherwise entries with where READ is NULL
187        // may not match either the query or its negation.
188        // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
189        String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?",
190                Calls.IS_READ, Calls.IS_READ, Calls.DATE);
191        List<String> selectionArgs = Lists.newArrayList(
192                Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW));
193        if (!isNew) {
194            // Negate the query.
195            selection = String.format("NOT (%s)", selection);
196        }
197        if (voicemailOnly) {
198            // Add a clause to fetch only items of type voicemail.
199            selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
200            selectionArgs.add(Integer.toString(Calls.VOICEMAIL_TYPE));
201        }
202        startQuery(token, requestId, Calls.CONTENT_URI_WITH_VOICEMAIL,
203                CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
204                Calls.DEFAULT_SORT_ORDER);
205    }
206
207    /** Cancel any pending fetch request. */
208    private void cancelFetch() {
209        cancelOperation(QUERY_NEW_CALLS_TOKEN);
210        cancelOperation(QUERY_OLD_CALLS_TOKEN);
211    }
212
213    /** Updates all new calls to mark them as old. */
214    public void markNewCallsAsOld() {
215        // Mark all "new" calls as not new anymore.
216        StringBuilder where = new StringBuilder();
217        where.append(Calls.NEW);
218        where.append(" = 1");
219
220        ContentValues values = new ContentValues(1);
221        values.put(Calls.NEW, "0");
222
223        startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
224                values, where.toString(), null);
225    }
226
227    /** Updates all new voicemails to mark them as old. */
228    public void markNewVoicemailsAsOld() {
229        // Mark all "new" voicemails as not new anymore.
230        StringBuilder where = new StringBuilder();
231        where.append(Calls.NEW);
232        where.append(" = 1 AND ");
233        where.append(Calls.TYPE);
234        where.append(" = ?");
235
236        ContentValues values = new ContentValues(1);
237        values.put(Calls.NEW, "0");
238
239        startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
240                values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
241    }
242
243    /** Updates all missed calls to mark them as read. */
244    public void markMissedCallsAsRead() {
245        // Mark all "new" calls as not new anymore.
246        StringBuilder where = new StringBuilder();
247        where.append(Calls.IS_READ).append(" = 0");
248        where.append(" AND ");
249        where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
250
251        ContentValues values = new ContentValues(1);
252        values.put(Calls.IS_READ, "1");
253
254        startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
255                where.toString(), null);
256    }
257
258    /**
259     * Start a new request and return its id. The request id will be used as the cookie for the
260     * background request.
261     * <p>
262     * Closes any open cursor that has not yet been sent to the requester.
263     */
264    private synchronized int newCallsRequest() {
265        MoreCloseables.closeQuietly(mNewCallsCursor);
266        MoreCloseables.closeQuietly(mOldCallsCursor);
267        mNewCallsCursor = null;
268        mOldCallsCursor = null;
269        return ++mCallsRequestId;
270    }
271
272    @Override
273    protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
274        if (token == QUERY_NEW_CALLS_TOKEN) {
275            int requestId = ((Integer) cookie).intValue();
276            if (requestId != mCallsRequestId) {
277                // Ignore this query since it does not correspond to the latest request.
278                return;
279            }
280
281            // Store the returned cursor.
282            MoreCloseables.closeQuietly(mNewCallsCursor);
283            mNewCallsCursor = new ExtendedCursor(
284                    cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
285        } else if (token == QUERY_OLD_CALLS_TOKEN) {
286            int requestId = ((Integer) cookie).intValue();
287            if (requestId != mCallsRequestId) {
288                // Ignore this query since it does not correspond to the latest request.
289                return;
290            }
291
292            // Store the returned cursor.
293            MoreCloseables.closeQuietly(mOldCallsCursor);
294            mOldCallsCursor = new ExtendedCursor(
295                    cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
296        } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
297            updateVoicemailStatus(cursor);
298            return;
299        } else {
300            Log.w(TAG, "Unknown query completed: ignoring: " + token);
301            return;
302        }
303
304        if (mNewCallsCursor != null && mOldCallsCursor != null) {
305            updateAdapterData(createMergedCursor());
306        }
307    }
308
309    /** Creates the merged cursor representing the data to show in the call log. */
310    @GuardedBy("this")
311    private Cursor createMergedCursor() {
312        try {
313            final boolean hasNewCalls = mNewCallsCursor.getCount() != 0;
314            final boolean hasOldCalls = mOldCallsCursor.getCount() != 0;
315
316            if (!hasNewCalls) {
317                // Return only the old calls, without the header.
318                MoreCloseables.closeQuietly(mNewCallsCursor);
319                return mOldCallsCursor;
320            }
321
322            if (!hasOldCalls) {
323                // Return only the new calls.
324                MoreCloseables.closeQuietly(mOldCallsCursor);
325                return new MergeCursor(
326                        new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor });
327            }
328
329            return new MergeCursor(new Cursor[]{
330                    createNewCallsHeaderCursor(), mNewCallsCursor,
331                    createOldCallsHeaderCursor(), mOldCallsCursor});
332        } finally {
333            // Any cursor still open is now owned, directly or indirectly, by the caller.
334            mNewCallsCursor = null;
335            mOldCallsCursor = null;
336        }
337    }
338
339    /**
340     * Updates the adapter in the call log fragment to show the new cursor data.
341     */
342    private void updateAdapterData(Cursor combinedCursor) {
343        final Listener listener = mListener.get();
344        if (listener != null) {
345            listener.onCallsFetched(combinedCursor);
346        }
347    }
348
349    private void updateVoicemailStatus(Cursor statusCursor) {
350        final Listener listener = mListener.get();
351        if (listener != null) {
352            listener.onVoicemailStatusFetched(statusCursor);
353        }
354    }
355
356    /** Listener to completion of various queries. */
357    public interface Listener {
358        /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
359        void onVoicemailStatusFetched(Cursor statusCursor);
360
361        /**
362         * Called when {@link CallLogQueryHandler#fetchAllCalls()} or
363         * {@link CallLogQueryHandler#fetchVoicemailOnly()} complete.
364         */
365        void onCallsFetched(Cursor combinedCursor);
366    }
367}
368