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