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