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.content.Context;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteDatabaseCorruptException;
25 import android.database.sqlite.SQLiteDiskIOException;
26 import android.database.sqlite.SQLiteException;
27 import android.database.sqlite.SQLiteFullException;
28 import android.net.Uri;
29 import android.os.Build;
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.provider.VoicemailContract.Voicemails;
36 import android.util.Log;
37 
38 import com.android.contacts.common.compat.SdkVersionOverride;
39 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
40 import com.android.contacts.common.util.PermissionsUtil;
41 import com.android.dialer.database.VoicemailArchiveContract;
42 import com.android.dialer.util.AppCompatConstants;
43 import com.android.dialer.util.TelecomUtil;
44 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
45 
46 import com.google.common.collect.Lists;
47 
48 import java.lang.ref.WeakReference;
49 import java.util.List;
50 
51 /** Handles asynchronous queries to the call log. */
52 public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
53     private static final String TAG = "CallLogQueryHandler";
54     private static final int NUM_LOGS_TO_DISPLAY = 1000;
55 
56     /** The token for the query to fetch the old entries from the call log. */
57     private static final int QUERY_CALLLOG_TOKEN = 54;
58     /** The token for the query to mark all missed calls as old after seeing the call log. */
59     private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
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 = 56;
62     /** The token for the query to fetch voicemail status messages. */
63     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
64     /** The token for the query to fetch the number of unread voicemails. */
65     private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
66     /** The token for the query to fetch the number of missed calls. */
67     private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
68     /** The oken for the query to fetch the archived voicemails. */
69     private static final int QUERY_VOICEMAIL_ARCHIVE = 60;
70 
71     private final int mLogLimit;
72 
73     /**
74      * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
75      * type. Exception: excludes Calls.VOICEMAIL_TYPE.
76      */
77     public static final int CALL_TYPE_ALL = -1;
78 
79     private final WeakReference<Listener> mListener;
80 
81     private final Context mContext;
82 
83     /**
84      * Simple handler that wraps background calls to catch
85      * {@link SQLiteException}, such as when the disk is full.
86      */
87     protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
CatchingWorkerHandler(Looper looper)88         public CatchingWorkerHandler(Looper looper) {
89             super(looper);
90         }
91 
92         @Override
handleMessage(Message msg)93         public void handleMessage(Message msg) {
94             try {
95                 // Perform same query while catching any exceptions
96                 super.handleMessage(msg);
97             } catch (SQLiteDiskIOException e) {
98                 Log.w(TAG, "Exception on background worker thread", e);
99             } catch (SQLiteFullException e) {
100                 Log.w(TAG, "Exception on background worker thread", e);
101             } catch (SQLiteDatabaseCorruptException e) {
102                 Log.w(TAG, "Exception on background worker thread", e);
103             } catch (IllegalArgumentException e) {
104                 Log.w(TAG, "ContactsProvider not present on device", e);
105             } catch (SecurityException e) {
106                 // Shouldn't happen if we are protecting the entry points correctly,
107                 // but just in case.
108                 Log.w(TAG, "No permission to access ContactsProvider.", e);
109             }
110         }
111     }
112 
113     @Override
createHandler(Looper looper)114     protected Handler createHandler(Looper looper) {
115         // Provide our special handler that catches exceptions
116         return new CatchingWorkerHandler(looper);
117     }
118 
CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener)119     public CallLogQueryHandler(Context context, ContentResolver contentResolver,
120             Listener listener) {
121         this(context, contentResolver, listener, -1);
122     }
123 
CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener, int limit)124     public CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener,
125             int limit) {
126         super(contentResolver);
127         mContext = context.getApplicationContext();
128         mListener = new WeakReference<Listener>(listener);
129         mLogLimit = limit;
130     }
131 
132     /**
133      * Fetch all the voicemails in the voicemail archive.
134      */
fetchVoicemailArchive()135     public void fetchVoicemailArchive() {
136         startQuery(QUERY_VOICEMAIL_ARCHIVE, null,
137                 VoicemailArchiveContract.VoicemailArchive.CONTENT_URI,
138                 null, VoicemailArchiveContract.VoicemailArchive.ARCHIVED + " = 1", null,
139                 VoicemailArchiveContract.VoicemailArchive.DATE + " DESC");
140     }
141 
142 
143     /**
144      * Fetches the list of calls from the call log for a given type.
145      * This call ignores the new or old state.
146      * <p>
147      * It will asynchronously update the content of the list view when the fetch completes.
148      */
fetchCalls(int callType, long newerThan)149     public void fetchCalls(int callType, long newerThan) {
150         cancelFetch();
151         if (PermissionsUtil.hasPhonePermissions(mContext)) {
152             fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
153         } else {
154             updateAdapterData(null);
155         }
156     }
157 
fetchCalls(int callType)158     public void fetchCalls(int callType) {
159         fetchCalls(callType, 0);
160     }
161 
fetchVoicemailStatus()162     public void fetchVoicemailStatus() {
163         if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
164             startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
165                     VoicemailStatusHelperImpl.PROJECTION, null, null, null);
166         }
167     }
168 
fetchVoicemailUnreadCount()169     public void fetchVoicemailUnreadCount() {
170         if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
171             // Only count voicemails that have not been read and have not been deleted.
172             startQuery(QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN, null, Voicemails.CONTENT_URI,
173                 new String[] { Voicemails._ID },
174                     Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0", null, null);
175         }
176     }
177 
178     /** Fetches the list of calls in the call log. */
fetchCalls(int token, int callType, boolean newOnly, long newerThan)179     private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
180         StringBuilder where = new StringBuilder();
181         List<String> selectionArgs = Lists.newArrayList();
182 
183         // Always hide blocked calls.
184         where.append("(").append(Calls.TYPE).append(" != ?)");
185         selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
186 
187         // Ignore voicemails marked as deleted
188         if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
189                 >= Build.VERSION_CODES.M) {
190             where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
191         }
192 
193         if (newOnly) {
194             where.append(" AND (").append(Calls.NEW).append(" = 1)");
195         }
196 
197         if (callType > CALL_TYPE_ALL) {
198             where.append(" AND (").append(Calls.TYPE).append(" = ?)");
199             selectionArgs.add(Integer.toString(callType));
200         } else {
201             where.append(" AND NOT ");
202             where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
203         }
204 
205         if (newerThan > 0) {
206             where.append(" AND (").append(Calls.DATE).append(" > ?)");
207             selectionArgs.add(Long.toString(newerThan));
208         }
209 
210         final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
211         final String selection = where.length() > 0 ? where.toString() : null;
212         Uri uri = TelecomUtil.getCallLogUri(mContext).buildUpon()
213                 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
214                 .build();
215         startQuery(token, null, uri, CallLogQuery._PROJECTION, selection, selectionArgs.toArray(
216                 new String[selectionArgs.size()]), Calls.DEFAULT_SORT_ORDER);
217     }
218 
219     /** Cancel any pending fetch request. */
cancelFetch()220     private void cancelFetch() {
221         cancelOperation(QUERY_CALLLOG_TOKEN);
222     }
223 
224     /** Updates all new calls to mark them as old. */
markNewCallsAsOld()225     public void markNewCallsAsOld() {
226         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
227             return;
228         }
229         // Mark all "new" calls as not new anymore.
230         StringBuilder where = new StringBuilder();
231         where.append(Calls.NEW);
232         where.append(" = 1");
233 
234         ContentValues values = new ContentValues(1);
235         values.put(Calls.NEW, "0");
236 
237         startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, TelecomUtil.getCallLogUri(mContext),
238                 values, where.toString(), null);
239     }
240 
241     /** Updates all missed calls to mark them as read. */
markMissedCallsAsRead()242     public void markMissedCallsAsRead() {
243         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
244             return;
245         }
246 
247         ContentValues values = new ContentValues(1);
248         values.put(Calls.IS_READ, "1");
249 
250         startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
251                 getUnreadMissedCallsQuery(), null);
252     }
253 
254     /** Fetch all missed calls received since last time the tab was opened. */
fetchMissedCallsUnreadCount()255     public void fetchMissedCallsUnreadCount() {
256         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
257             return;
258         }
259 
260         startQuery(QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN, null, Calls.CONTENT_URI,
261                 new String[]{Calls._ID}, getUnreadMissedCallsQuery(), null, null);
262     }
263 
264 
265     @Override
onNotNullableQueryComplete(int token, Object cookie, Cursor cursor)266     protected synchronized void onNotNullableQueryComplete(int token, Object cookie,
267             Cursor cursor) {
268         if (cursor == null) {
269             return;
270         }
271         try {
272             if (token == QUERY_CALLLOG_TOKEN || token == QUERY_VOICEMAIL_ARCHIVE) {
273                 if (updateAdapterData(cursor)) {
274                     cursor = null;
275                 }
276             } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
277                 updateVoicemailStatus(cursor);
278             } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
279                 updateVoicemailUnreadCount(cursor);
280             } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
281                 updateMissedCallsUnreadCount(cursor);
282             } else {
283                 Log.w(TAG, "Unknown query completed: ignoring: " + token);
284             }
285         } finally {
286             if (cursor != null) {
287                 cursor.close();
288             }
289         }
290     }
291 
292     /**
293      * Updates the adapter in the call log fragment to show the new cursor data.
294      * Returns true if the listener took ownership of the cursor.
295      */
updateAdapterData(Cursor cursor)296     private boolean updateAdapterData(Cursor cursor) {
297         final Listener listener = mListener.get();
298         if (listener != null) {
299             return listener.onCallsFetched(cursor);
300         }
301         return false;
302 
303     }
304 
305     /**
306      * @return Query string to get all unread missed calls.
307      */
getUnreadMissedCallsQuery()308     private String getUnreadMissedCallsQuery() {
309         StringBuilder where = new StringBuilder();
310         where.append(Calls.IS_READ).append(" = 0 OR ").append(Calls.IS_READ).append(" IS NULL");
311         where.append(" AND ");
312         where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
313         return where.toString();
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 
updateVoicemailUnreadCount(Cursor statusCursor)323     private void updateVoicemailUnreadCount(Cursor statusCursor) {
324         final Listener listener = mListener.get();
325         if (listener != null) {
326             listener.onVoicemailUnreadCountFetched(statusCursor);
327         }
328     }
329 
updateMissedCallsUnreadCount(Cursor statusCursor)330     private void updateMissedCallsUnreadCount(Cursor statusCursor) {
331         final Listener listener = mListener.get();
332         if (listener != null) {
333             listener.onMissedCallsUnreadCountFetched(statusCursor);
334         }
335     }
336 
337     /** Listener to completion of various queries. */
338     public interface Listener {
339         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
onVoicemailStatusFetched(Cursor statusCursor)340         void onVoicemailStatusFetched(Cursor statusCursor);
341 
342         /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
onVoicemailUnreadCountFetched(Cursor cursor)343         void onVoicemailUnreadCountFetched(Cursor cursor);
344 
345         /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
onMissedCallsUnreadCountFetched(Cursor cursor)346         void onMissedCallsUnreadCountFetched(Cursor cursor);
347 
348         /**
349          * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete.
350          * Returns true if takes ownership of cursor.
351          */
onCallsFetched(Cursor combinedCursor)352         boolean onCallsFetched(Cursor combinedCursor);
353     }
354 }
355