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