• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.contacts.calllog;
18 
19 import com.android.common.io.MoreCloseables;
20 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
21 import com.google.android.collect.Lists;
22 
23 import android.content.AsyncQueryHandler;
24 import android.content.ContentResolver;
25 import android.content.ContentValues;
26 import android.database.Cursor;
27 import android.database.MatrixCursor;
28 import android.database.MergeCursor;
29 import android.database.sqlite.SQLiteDatabaseCorruptException;
30 import android.database.sqlite.SQLiteDiskIOException;
31 import android.database.sqlite.SQLiteException;
32 import android.database.sqlite.SQLiteFullException;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.provider.CallLog.Calls;
37 import android.provider.VoicemailContract.Status;
38 import android.util.Log;
39 
40 import java.lang.ref.WeakReference;
41 import java.util.List;
42 import java.util.concurrent.TimeUnit;
43 
44 import 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 {
CatchingWorkerHandler(Looper looper)101         public CatchingWorkerHandler(Looper looper) {
102             super(looper);
103         }
104 
105         @Override
handleMessage(Message msg)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
createHandler(Looper looper)121     protected Handler createHandler(Looper looper) {
122         // Provide our special handler that catches exceptions
123         return new CatchingWorkerHandler(looper);
124     }
125 
CallLogQueryHandler(ContentResolver contentResolver, Listener listener)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. */
createHeaderCursorFor(int section)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. */
createOldCallsHeaderCursor()145     private Cursor createOldCallsHeaderCursor() {
146         return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
147     }
148 
149     /** Returns a cursor for the new calls header. */
createNewCallsHeaderCursor()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      */
fetchAllCalls()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      */
fetchVoicemailOnly()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 
fetchVoicemailStatus()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. */
fetchCalls(int token, int requestId, boolean isNew, boolean voicemailOnly)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. */
cancelFetch()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. */
markNewCallsAsOld()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. */
markNewVoicemailsAsOld()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. */
markMissedCallsAsRead()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      */
newCallsRequest()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
onQueryComplete(int token, Object cookie, Cursor cursor)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")
createMergedCursor()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      */
updateAdapterData(Cursor combinedCursor)342     private void updateAdapterData(Cursor combinedCursor) {
343         final Listener listener = mListener.get();
344         if (listener != null) {
345             listener.onCallsFetched(combinedCursor);
346         }
347     }
348 
updateVoicemailStatus(Cursor statusCursor)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. */
onVoicemailStatusFetched(Cursor statusCursor)359         void onVoicemailStatusFetched(Cursor statusCursor);
360 
361         /**
362          * Called when {@link CallLogQueryHandler#fetchAllCalls()} or
363          * {@link CallLogQueryHandler#fetchVoicemailOnly()} complete.
364          */
onCallsFetched(Cursor combinedCursor)365         void onCallsFetched(Cursor combinedCursor);
366     }
367 }
368