• 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 
63     /** The token for the query to fetch voicemail status messages. */
64     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
65 
66     /**
67      * The time window from the current time within which an unread entry will be added to the new
68      * section.
69      */
70     private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7);
71 
72     private final WeakReference<Listener> mListener;
73 
74     /** The cursor containing the new calls, or null if they have not yet been fetched. */
75     @GuardedBy("this") private Cursor mNewCallsCursor;
76     /** The cursor containing the old calls, or null if they have not yet been fetched. */
77     @GuardedBy("this") private Cursor mOldCallsCursor;
78 
79     /**
80      * Simple handler that wraps background calls to catch
81      * {@link SQLiteException}, such as when the disk is full.
82      */
83     protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
CatchingWorkerHandler(Looper looper)84         public CatchingWorkerHandler(Looper looper) {
85             super(looper);
86         }
87 
88         @Override
handleMessage(Message msg)89         public void handleMessage(Message msg) {
90             try {
91                 // Perform same query while catching any exceptions
92                 super.handleMessage(msg);
93             } catch (SQLiteDiskIOException e) {
94                 Log.w(TAG, "Exception on background worker thread", e);
95             } catch (SQLiteFullException e) {
96                 Log.w(TAG, "Exception on background worker thread", e);
97             } catch (SQLiteDatabaseCorruptException e) {
98                 Log.w(TAG, "Exception on background worker thread", e);
99             }
100         }
101     }
102 
103     @Override
createHandler(Looper looper)104     protected Handler createHandler(Looper looper) {
105         // Provide our special handler that catches exceptions
106         return new CatchingWorkerHandler(looper);
107     }
108 
CallLogQueryHandler(ContentResolver contentResolver, Listener listener)109     public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
110         super(contentResolver);
111         mListener = new WeakReference<Listener>(listener);
112     }
113 
114     /** Creates a cursor that contains a single row and maps the section to the given value. */
createHeaderCursorFor(int section)115     private Cursor createHeaderCursorFor(int section) {
116         MatrixCursor matrixCursor =
117                 new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
118         // The values in this row correspond to default values for _PROJECTION from CallLogQuery
119         // plus the section value.
120         matrixCursor.addRow(new Object[]{
121                 0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0,
122                 section
123         });
124         return matrixCursor;
125     }
126 
127     /** Returns a cursor for the old calls header. */
createOldCallsHeaderCursor()128     private Cursor createOldCallsHeaderCursor() {
129         return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
130     }
131 
132     /** Returns a cursor for the new calls header. */
createNewCallsHeaderCursor()133     private Cursor createNewCallsHeaderCursor() {
134         return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER);
135     }
136 
137     /**
138      * Fetches the list of calls from the call log.
139      * <p>
140      * It will asynchronously update the content of the list view when the fetch completes.
141      */
fetchAllCalls()142     public void fetchAllCalls() {
143         cancelFetch();
144         invalidate();
145         fetchCalls(QUERY_NEW_CALLS_TOKEN, true /*isNew*/, false /*voicemailOnly*/);
146         fetchCalls(QUERY_OLD_CALLS_TOKEN, false /*isNew*/, false /*voicemailOnly*/);
147     }
148 
149     /**
150      * Fetches the list of calls from the call log but include only the voicemail.
151      * <p>
152      * It will asynchronously update the content of the list view when the fetch completes.
153      */
fetchVoicemailOnly()154     public void fetchVoicemailOnly() {
155         cancelFetch();
156         invalidate();
157         fetchCalls(QUERY_NEW_CALLS_TOKEN, true /*isNew*/, true /*voicemailOnly*/);
158         fetchCalls(QUERY_OLD_CALLS_TOKEN, false /*isNew*/, true /*voicemailOnly*/);
159     }
160 
161 
fetchVoicemailStatus()162     public void fetchVoicemailStatus() {
163         startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
164                 VoicemailStatusHelperImpl.PROJECTION, null, null, null);
165     }
166 
167     /** Fetches the list of calls in the call log, either the new one or the old ones. */
fetchCalls(int token, boolean isNew, boolean voicemailOnly)168     private void fetchCalls(int token, boolean isNew, boolean voicemailOnly) {
169         // We need to check for NULL explicitly otherwise entries with where READ is NULL
170         // may not match either the query or its negation.
171         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
172         String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?",
173                 Calls.IS_READ, Calls.IS_READ, Calls.DATE);
174         List<String> selectionArgs = Lists.newArrayList(
175                 Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW));
176         if (!isNew) {
177             // Negate the query.
178             selection = String.format("NOT (%s)", selection);
179         }
180         if (voicemailOnly) {
181             // Add a clause to fetch only items of type voicemail.
182             selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
183             selectionArgs.add(Integer.toString(Calls.VOICEMAIL_TYPE));
184         }
185         startQuery(token, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
186                 CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
187                 Calls.DEFAULT_SORT_ORDER);
188     }
189 
190     /** Cancel any pending fetch request. */
cancelFetch()191     private void cancelFetch() {
192         cancelOperation(QUERY_NEW_CALLS_TOKEN);
193         cancelOperation(QUERY_OLD_CALLS_TOKEN);
194     }
195 
196     /** Updates all new calls to mark them as old. */
markNewCallsAsOld()197     public void markNewCallsAsOld() {
198         // Mark all "new" calls as not new anymore.
199         StringBuilder where = new StringBuilder();
200         where.append(Calls.NEW);
201         where.append(" = 1");
202 
203         ContentValues values = new ContentValues(1);
204         values.put(Calls.NEW, "0");
205 
206         startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
207                 values, where.toString(), null);
208     }
209 
210     /** Updates all new voicemails to mark them as old. */
markNewVoicemailsAsOld()211     public void markNewVoicemailsAsOld() {
212         // Mark all "new" voicemails as not new anymore.
213         StringBuilder where = new StringBuilder();
214         where.append(Calls.NEW);
215         where.append(" = 1 AND ");
216         where.append(Calls.TYPE);
217         where.append(" = ?");
218 
219         ContentValues values = new ContentValues(1);
220         values.put(Calls.NEW, "0");
221 
222         startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
223                 values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
224     }
225 
226     /** Updates all missed calls to mark them as read. */
markMissedCallsAsRead()227     public void markMissedCallsAsRead() {
228         // Mark all "new" calls as not new anymore.
229         StringBuilder where = new StringBuilder();
230         where.append(Calls.IS_READ).append(" = 0");
231         where.append(" AND ");
232         where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
233 
234         ContentValues values = new ContentValues(1);
235         values.put(Calls.IS_READ, "1");
236 
237         startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
238                 where.toString(), null);
239     }
240 
241     /**
242      * Invalidate the current list of calls.
243      * <p>
244      * This method is synchronized because it must close the cursors and reset them atomically.
245      */
invalidate()246     private synchronized void invalidate() {
247         MoreCloseables.closeQuietly(mNewCallsCursor);
248         MoreCloseables.closeQuietly(mOldCallsCursor);
249         mNewCallsCursor = null;
250         mOldCallsCursor = null;
251     }
252 
253     @Override
onQueryComplete(int token, Object cookie, Cursor cursor)254     protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
255         if (token == QUERY_NEW_CALLS_TOKEN) {
256             // Store the returned cursor.
257             mNewCallsCursor = new ExtendedCursor(
258                     cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
259         } else if (token == QUERY_OLD_CALLS_TOKEN) {
260             // Store the returned cursor.
261             mOldCallsCursor = new ExtendedCursor(
262                     cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
263         } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
264             updateVoicemailStatus(cursor);
265             return;
266         } else {
267             Log.w(TAG, "Unknown query completed: ignoring: " + token);
268             return;
269         }
270 
271         if (mNewCallsCursor != null && mOldCallsCursor != null) {
272             updateAdapterData(createMergedCursor());
273         }
274     }
275 
276     /** Creates the merged cursor representing the data to show in the call log. */
277     @GuardedBy("this")
createMergedCursor()278     private Cursor createMergedCursor() {
279         try {
280             final boolean hasNewCalls = mNewCallsCursor.getCount() != 0;
281             final boolean hasOldCalls = mOldCallsCursor.getCount() != 0;
282 
283             if (!hasNewCalls) {
284                 // Return only the old calls, without the header.
285                 MoreCloseables.closeQuietly(mNewCallsCursor);
286                 return mOldCallsCursor;
287             }
288 
289             if (!hasOldCalls) {
290                 // Return only the new calls.
291                 MoreCloseables.closeQuietly(mOldCallsCursor);
292                 return new MergeCursor(
293                         new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor });
294             }
295 
296             return new MergeCursor(new Cursor[]{
297                     createNewCallsHeaderCursor(), mNewCallsCursor,
298                     createOldCallsHeaderCursor(), mOldCallsCursor});
299         } finally {
300             // Any cursor still open is now owned, directly or indirectly, by the caller.
301             mNewCallsCursor = null;
302             mOldCallsCursor = null;
303         }
304     }
305 
306     /**
307      * Updates the adapter in the call log fragment to show the new cursor data.
308      */
updateAdapterData(Cursor combinedCursor)309     private void updateAdapterData(Cursor combinedCursor) {
310         final Listener listener = mListener.get();
311         if (listener != null) {
312             listener.onCallsFetched(combinedCursor);
313         }
314     }
315 
updateVoicemailStatus(Cursor statusCursor)316     private void updateVoicemailStatus(Cursor statusCursor) {
317         final Listener listener = mListener.get();
318         if (listener != null) {
319             listener.onVoicemailStatusFetched(statusCursor);
320         }
321     }
322 
323     /** Listener to completion of various queries. */
324     public interface Listener {
325         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
onVoicemailStatusFetched(Cursor statusCursor)326         void onVoicemailStatusFetched(Cursor statusCursor);
327 
328         /**
329          * Called when {@link CallLogQueryHandler#fetchAllCalls()} or
330          * {@link CallLogQueryHandler#fetchVoicemailOnly()} complete.
331          */
onCallsFetched(Cursor combinedCursor)332         void onCallsFetched(Cursor combinedCursor);
333     }
334 }
335