• 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.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.dialer.voicemailstatus.VoicemailStatusQuery;
44 import com.android.voicemail.VoicemailComponent;
45 import java.lang.ref.WeakReference;
46 import java.util.ArrayList;
47 import java.util.List;
48 
49 /** Handles asynchronous queries to the call log. */
50 public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
51 
52   /**
53    * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
54    * type. Exception: excludes Calls.VOICEMAIL_TYPE.
55    */
56   public static final int CALL_TYPE_ALL = -1;
57 
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 read after seeing the call log. */
62   private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 56;
63   /** The token for the query to fetch voicemail status messages. */
64   private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
65   /** The token for the query to fetch the number of unread voicemails. */
66   private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
67   /** The token for the query to fetch the number of missed calls. */
68   private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
69 
70   private final int logLimit;
71   private final WeakReference<Listener> listener;
72 
73   private final Context context;
74 
CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener)75   public CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener) {
76     this(context, contentResolver, listener, -1);
77   }
78 
CallLogQueryHandler( Context context, ContentResolver contentResolver, Listener listener, int limit)79   public CallLogQueryHandler(
80       Context context, ContentResolver contentResolver, Listener listener, int limit) {
81     super(contentResolver);
82     this.context = context.getApplicationContext();
83     this.listener = new WeakReference<>(listener);
84     logLimit = limit;
85   }
86 
87   @Override
createHandler(Looper looper)88   protected Handler createHandler(Looper looper) {
89     // Provide our special handler that catches exceptions
90     return new CatchingWorkerHandler(looper);
91   }
92 
93   /**
94    * Fetches the list of calls from the call log for a given type. This call ignores the new or old
95    * state.
96    *
97    * <p>It will asynchronously update the content of the list view when the fetch completes.
98    */
fetchCalls(int callType, long newerThan)99   public void fetchCalls(int callType, long newerThan) {
100     cancelFetch();
101     if (PermissionsUtil.hasPhonePermissions(context)) {
102       fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
103     } else {
104       updateAdapterData(null);
105     }
106   }
107 
fetchVoicemailStatus()108   public void fetchVoicemailStatus() {
109     StringBuilder where = new StringBuilder();
110     List<String> selectionArgs = new ArrayList<>();
111 
112     VoicemailComponent.get(context)
113         .getVoicemailClient()
114         .appendOmtpVoicemailStatusSelectionClause(context, where, selectionArgs);
115 
116     if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
117       startQuery(
118           QUERY_VOICEMAIL_STATUS_TOKEN,
119           null,
120           Status.CONTENT_URI,
121           VoicemailStatusQuery.getProjection(),
122           where.toString(),
123           selectionArgs.toArray(new String[selectionArgs.size()]),
124           null);
125     }
126   }
127 
fetchVoicemailUnreadCount()128   public void fetchVoicemailUnreadCount() {
129     if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
130       // Only count voicemails that have not been read and have not been deleted.
131       StringBuilder where =
132           new StringBuilder(Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0 ");
133       List<String> selectionArgs = new ArrayList<>();
134 
135       VoicemailComponent.get(context)
136           .getVoicemailClient()
137           .appendOmtpVoicemailSelectionClause(context, where, selectionArgs);
138 
139       startQuery(
140           QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN,
141           null,
142           Voicemails.CONTENT_URI,
143           new String[] {Voicemails._ID},
144           where.toString(),
145           selectionArgs.toArray(new String[selectionArgs.size()]),
146           null);
147     }
148   }
149 
150   /** Fetches the list of calls in the call log. */
fetchCalls(int token, int callType, boolean newOnly, long newerThan)151   private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
152     StringBuilder where = new StringBuilder();
153     List<String> selectionArgs = new ArrayList<>();
154 
155     // Always hide blocked calls.
156     where.append("(").append(Calls.TYPE).append(" != ?)");
157     selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
158 
159     // Ignore voicemails marked as deleted
160     if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
161       where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
162     }
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 + " = " + AppCompatConstants.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