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.dialer.calllog;
18 
19 import android.content.AsyncQueryHandler;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.database.Cursor;
23 import android.database.MatrixCursor;
24 import android.database.MergeCursor;
25 import android.database.sqlite.SQLiteDatabaseCorruptException;
26 import android.database.sqlite.SQLiteDiskIOException;
27 import android.database.sqlite.SQLiteException;
28 import android.database.sqlite.SQLiteFullException;
29 import android.net.Uri;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.provider.CallLog.Calls;
34 import android.provider.VoicemailContract.Status;
35 import android.util.Log;
36 
37 import com.android.common.io.MoreCloseables;
38 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
39 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
40 import com.google.common.collect.Lists;
41 
42 import java.lang.ref.WeakReference;
43 import java.util.List;
44 
45 /** Handles asynchronous queries to the call log. */
46 public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
47     private static final String[] EMPTY_STRING_ARRAY = new String[0];
48 
49     private static final String TAG = "CallLogQueryHandler";
50     private static final int NUM_LOGS_TO_DISPLAY = 1000;
51 
52     /** The token for the query to fetch the old entries from the call log. */
53     private static final int QUERY_CALLLOG_TOKEN = 54;
54     /** The token for the query to mark all missed calls as old after seeing the call log. */
55     private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
56     /** The token for the query to mark all new voicemails as old. */
57     private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
58     /** The token for the query to mark all missed calls as read after seeing the call log. */
59     private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
60     /** The token for the query to fetch voicemail status messages. */
61     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
62 
63     private final int mLogLimit;
64 
65     /**
66      * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
67      * type.
68      */
69     public static final int CALL_TYPE_ALL = -1;
70 
71     private final WeakReference<Listener> mListener;
72 
73     /**
74      * Simple handler that wraps background calls to catch
75      * {@link SQLiteException}, such as when the disk is full.
76      */
77     protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
CatchingWorkerHandler(Looper looper)78         public CatchingWorkerHandler(Looper looper) {
79             super(looper);
80         }
81 
82         @Override
handleMessage(Message msg)83         public void handleMessage(Message msg) {
84             try {
85                 // Perform same query while catching any exceptions
86                 super.handleMessage(msg);
87             } catch (SQLiteDiskIOException e) {
88                 Log.w(TAG, "Exception on background worker thread", e);
89             } catch (SQLiteFullException e) {
90                 Log.w(TAG, "Exception on background worker thread", e);
91             } catch (SQLiteDatabaseCorruptException e) {
92                 Log.w(TAG, "Exception on background worker thread", e);
93             } catch (IllegalArgumentException e) {
94                 Log.w(TAG, "ContactsProvider not present on device", e);
95             }
96         }
97     }
98 
99     @Override
createHandler(Looper looper)100     protected Handler createHandler(Looper looper) {
101         // Provide our special handler that catches exceptions
102         return new CatchingWorkerHandler(looper);
103     }
104 
CallLogQueryHandler(ContentResolver contentResolver, Listener listener)105     public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
106         this(contentResolver, listener, -1);
107     }
108 
CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit)109     public CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit) {
110         super(contentResolver);
111         mListener = new WeakReference<Listener>(listener);
112         mLogLimit = limit;
113     }
114 
115     /**
116      * Fetches the list of calls from the call log for a given type.
117      * This call ignores the new or old state.
118      * <p>
119      * It will asynchronously update the content of the list view when the fetch completes.
120      */
fetchCalls(int callType, long newerThan)121     public void fetchCalls(int callType, long newerThan) {
122         cancelFetch();
123         fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
124     }
125 
fetchCalls(int callType)126     public void fetchCalls(int callType) {
127         fetchCalls(callType, 0);
128     }
129 
fetchVoicemailStatus()130     public void fetchVoicemailStatus() {
131         startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
132                 VoicemailStatusHelperImpl.PROJECTION, null, null, null);
133     }
134 
135     /** Fetches the list of calls in the call log. */
fetchCalls(int token, int callType, boolean newOnly, long newerThan)136     private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
137         // We need to check for NULL explicitly otherwise entries with where READ is NULL
138         // may not match either the query or its negation.
139         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
140         StringBuilder where = new StringBuilder();
141         List<String> selectionArgs = Lists.newArrayList();
142 
143         if (newOnly) {
144             where.append(Calls.NEW);
145             where.append(" = 1");
146         }
147 
148         if (callType > CALL_TYPE_ALL) {
149             if (where.length() > 0) {
150                 where.append(" AND ");
151             }
152             // Add a clause to fetch only items of type voicemail.
153             where.append(String.format("(%s = ?)", Calls.TYPE));
154             // Add a clause to fetch only items newer than the requested date
155             selectionArgs.add(Integer.toString(callType));
156         }
157 
158         if (newerThan > 0) {
159             if (where.length() > 0) {
160                 where.append(" AND ");
161             }
162             where.append(String.format("(%s > ?)", Calls.DATE));
163             selectionArgs.add(Long.toString(newerThan));
164         }
165 
166         final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
167         final String selection = where.length() > 0 ? where.toString() : null;
168         Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
169                 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
170                 .build();
171         startQuery(token, null, uri,
172                 CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
173                 Calls.DEFAULT_SORT_ORDER);
174     }
175 
176     /** Cancel any pending fetch request. */
cancelFetch()177     private void cancelFetch() {
178         cancelOperation(QUERY_CALLLOG_TOKEN);
179     }
180 
181     /** Updates all new calls to mark them as old. */
markNewCallsAsOld()182     public void markNewCallsAsOld() {
183         // Mark all "new" calls as not new anymore.
184         StringBuilder where = new StringBuilder();
185         where.append(Calls.NEW);
186         where.append(" = 1");
187 
188         ContentValues values = new ContentValues(1);
189         values.put(Calls.NEW, "0");
190 
191         startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
192                 values, where.toString(), null);
193     }
194 
195     /** Updates all new voicemails to mark them as old. */
markNewVoicemailsAsOld()196     public void markNewVoicemailsAsOld() {
197         // Mark all "new" voicemails as not new anymore.
198         StringBuilder where = new StringBuilder();
199         where.append(Calls.NEW);
200         where.append(" = 1 AND ");
201         where.append(Calls.TYPE);
202         where.append(" = ?");
203 
204         ContentValues values = new ContentValues(1);
205         values.put(Calls.NEW, "0");
206 
207         startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
208                 values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
209     }
210 
211     /** Updates all missed calls to mark them as read. */
markMissedCallsAsRead()212     public void markMissedCallsAsRead() {
213         // Mark all "new" calls as not new anymore.
214         StringBuilder where = new StringBuilder();
215         where.append(Calls.IS_READ).append(" = 0");
216         where.append(" AND ");
217         where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
218 
219         ContentValues values = new ContentValues(1);
220         values.put(Calls.IS_READ, "1");
221 
222         startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
223                 where.toString(), null);
224     }
225 
226     @Override
onNotNullableQueryComplete(int token, Object cookie, Cursor cursor)227     protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
228         if (cursor == null) {
229             return;
230         }
231         try {
232             if (token == QUERY_CALLLOG_TOKEN) {
233                 if (updateAdapterData(cursor)) {
234                     cursor = null;
235                 }
236             } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
237                 updateVoicemailStatus(cursor);
238             } else {
239                 Log.w(TAG, "Unknown query completed: ignoring: " + token);
240             }
241         } finally {
242             if (cursor != null) {
243                 cursor.close();
244             }
245         }
246     }
247 
248     /**
249      * Updates the adapter in the call log fragment to show the new cursor data.
250      * Returns true if the listener took ownership of the cursor.
251      */
updateAdapterData(Cursor cursor)252     private boolean updateAdapterData(Cursor cursor) {
253         final Listener listener = mListener.get();
254         if (listener != null) {
255             return listener.onCallsFetched(cursor);
256         }
257         return false;
258 
259     }
260 
updateVoicemailStatus(Cursor statusCursor)261     private void updateVoicemailStatus(Cursor statusCursor) {
262         final Listener listener = mListener.get();
263         if (listener != null) {
264             listener.onVoicemailStatusFetched(statusCursor);
265         }
266     }
267 
268     /** Listener to completion of various queries. */
269     public interface Listener {
270         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
onVoicemailStatusFetched(Cursor statusCursor)271         void onVoicemailStatusFetched(Cursor statusCursor);
272 
273         /**
274          * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete.
275          * Returns true if takes ownership of cursor.
276          */
onCallsFetched(Cursor combinedCursor)277         boolean onCallsFetched(Cursor combinedCursor);
278     }
279 }
280