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