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