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.database.Cursor; 23 import android.database.MatrixCursor; 24 import android.database.MergeCursor; 25 import android.database.sqlite.SQLiteDatabaseCorruptException; 26 import android.database.sqlite.SQLiteDiskIOException; 27 import android.database.sqlite.SQLiteException; 28 import android.database.sqlite.SQLiteFullException; 29 import android.net.Uri; 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.util.Log; 36 37 import com.android.common.io.MoreCloseables; 38 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler; 39 import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 40 import com.google.common.collect.Lists; 41 42 import java.lang.ref.WeakReference; 43 import java.util.List; 44 import java.util.concurrent.TimeUnit; 45 46 import javax.annotation.concurrent.GuardedBy; 47 48 /** Handles asynchronous queries to the call log. */ 49 /*package*/ class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { 50 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 51 52 private static final String TAG = "CallLogQueryHandler"; 53 private static final int NUM_LOGS_TO_DISPLAY = 1000; 54 55 /** The token for the query to fetch the new entries from the call log. */ 56 private static final int QUERY_NEW_CALLS_TOKEN = 53; 57 /** The token for the query to fetch the old entries from the call log. */ 58 private static final int QUERY_OLD_CALLS_TOKEN = 54; 59 /** The token for the query to mark all missed calls as old after seeing the call log. */ 60 private static final int UPDATE_MARK_AS_OLD_TOKEN = 55; 61 /** The token for the query to mark all new voicemails as old. */ 62 private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56; 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 = 57; 65 /** The token for the query to fetch voicemail status messages. */ 66 private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58; 67 68 /** 69 * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular 70 * type. 71 */ 72 public static final int CALL_TYPE_ALL = -1; 73 74 /** 75 * The time window from the current time within which an unread entry will be added to the new 76 * section. 77 */ 78 private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7); 79 80 private final WeakReference<Listener> mListener; 81 82 /** The cursor containing the new calls, or null if they have not yet been fetched. */ 83 @GuardedBy("this") private Cursor mNewCallsCursor; 84 /** The cursor containing the old calls, or null if they have not yet been fetched. */ 85 @GuardedBy("this") private Cursor mOldCallsCursor; 86 /** 87 * The identifier of the latest calls request. 88 * <p> 89 * A request for the list of calls requires two queries and hence the two cursor 90 * {@link #mNewCallsCursor} and {@link #mOldCallsCursor} above, corresponding to 91 * {@link #QUERY_NEW_CALLS_TOKEN} and {@link #QUERY_OLD_CALLS_TOKEN}. 92 * <p> 93 * When a new request is about to be started, existing cursors are closed. However, it is 94 * possible that one of the queries completes after the new request has started. This means that 95 * we might merge two cursors that do not correspond to the same request. Moreover, this may 96 * lead to a resource leak if the same query completes and we override the cursor without 97 * closing it first. 98 * <p> 99 * To make sure we only join two cursors from the same request, we use this variable to store 100 * the request id of the latest request and make sure we only process cursors corresponding to 101 * the this request. 102 */ 103 @GuardedBy("this") private int mCallsRequestId; 104 105 /** 106 * Simple handler that wraps background calls to catch 107 * {@link SQLiteException}, such as when the disk is full. 108 */ 109 protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { 110 public CatchingWorkerHandler(Looper looper) { 111 super(looper); 112 } 113 114 @Override 115 public void handleMessage(Message msg) { 116 try { 117 // Perform same query while catching any exceptions 118 super.handleMessage(msg); 119 } catch (SQLiteDiskIOException e) { 120 Log.w(TAG, "Exception on background worker thread", e); 121 } catch (SQLiteFullException e) { 122 Log.w(TAG, "Exception on background worker thread", e); 123 } catch (SQLiteDatabaseCorruptException e) { 124 Log.w(TAG, "Exception on background worker thread", e); 125 } 126 } 127 } 128 129 @Override 130 protected Handler createHandler(Looper looper) { 131 // Provide our special handler that catches exceptions 132 return new CatchingWorkerHandler(looper); 133 } 134 135 public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) { 136 super(contentResolver); 137 mListener = new WeakReference<Listener>(listener); 138 } 139 140 /** Creates a cursor that contains a single row and maps the section to the given value. */ 141 private Cursor createHeaderCursorFor(int section) { 142 MatrixCursor matrixCursor = 143 new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION); 144 // The values in this row correspond to default values for _PROJECTION from CallLogQuery 145 // plus the section value. 146 matrixCursor.addRow(new Object[]{ 147 0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0, 148 section 149 }); 150 return matrixCursor; 151 } 152 153 /** Returns a cursor for the old calls header. */ 154 private Cursor createOldCallsHeaderCursor() { 155 return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER); 156 } 157 158 /** Returns a cursor for the new calls header. */ 159 private Cursor createNewCallsHeaderCursor() { 160 return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER); 161 } 162 163 /** 164 * Fetches the list of calls from the call log for a given type. 165 * <p> 166 * It will asynchronously update the content of the list view when the fetch completes. 167 */ 168 public void fetchCalls(int callType) { 169 cancelFetch(); 170 int requestId = newCallsRequest(); 171 fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, callType); 172 fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, callType); 173 } 174 175 public void fetchVoicemailStatus() { 176 startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI, 177 VoicemailStatusHelperImpl.PROJECTION, null, null, null); 178 } 179 180 /** Fetches the list of calls in the call log, either the new one or the old ones. */ 181 private void fetchCalls(int token, int requestId, boolean isNew, int callType) { 182 // We need to check for NULL explicitly otherwise entries with where READ is NULL 183 // may not match either the query or its negation. 184 // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new". 185 String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?", 186 Calls.IS_READ, Calls.IS_READ, Calls.DATE); 187 List<String> selectionArgs = Lists.newArrayList( 188 Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW)); 189 if (!isNew) { 190 // Negate the query. 191 selection = String.format("NOT (%s)", selection); 192 } 193 if (callType > CALL_TYPE_ALL) { 194 // Add a clause to fetch only items of type voicemail. 195 selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE); 196 selectionArgs.add(Integer.toString(callType)); 197 } 198 Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon() 199 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(NUM_LOGS_TO_DISPLAY)) 200 .build(); 201 startQuery(token, requestId, uri, 202 CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY), 203 Calls.DEFAULT_SORT_ORDER); 204 } 205 206 /** Cancel any pending fetch request. */ 207 private void cancelFetch() { 208 cancelOperation(QUERY_NEW_CALLS_TOKEN); 209 cancelOperation(QUERY_OLD_CALLS_TOKEN); 210 } 211 212 /** Updates all new calls to mark them as old. */ 213 public void markNewCallsAsOld() { 214 // Mark all "new" calls as not new anymore. 215 StringBuilder where = new StringBuilder(); 216 where.append(Calls.NEW); 217 where.append(" = 1"); 218 219 ContentValues values = new ContentValues(1); 220 values.put(Calls.NEW, "0"); 221 222 startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 223 values, where.toString(), null); 224 } 225 226 /** Updates all new voicemails to mark them as old. */ 227 public void markNewVoicemailsAsOld() { 228 // Mark all "new" voicemails as not new anymore. 229 StringBuilder where = new StringBuilder(); 230 where.append(Calls.NEW); 231 where.append(" = 1 AND "); 232 where.append(Calls.TYPE); 233 where.append(" = ?"); 234 235 ContentValues values = new ContentValues(1); 236 values.put(Calls.NEW, "0"); 237 238 startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 239 values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) }); 240 } 241 242 /** Updates all missed calls to mark them as read. */ 243 public void markMissedCallsAsRead() { 244 // Mark all "new" calls as not new anymore. 245 StringBuilder where = new StringBuilder(); 246 where.append(Calls.IS_READ).append(" = 0"); 247 where.append(" AND "); 248 where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE); 249 250 ContentValues values = new ContentValues(1); 251 values.put(Calls.IS_READ, "1"); 252 253 startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values, 254 where.toString(), null); 255 } 256 257 /** 258 * Start a new request and return its id. The request id will be used as the cookie for the 259 * background request. 260 * <p> 261 * Closes any open cursor that has not yet been sent to the requester. 262 */ 263 private synchronized int newCallsRequest() { 264 MoreCloseables.closeQuietly(mNewCallsCursor); 265 MoreCloseables.closeQuietly(mOldCallsCursor); 266 mNewCallsCursor = null; 267 mOldCallsCursor = null; 268 return ++mCallsRequestId; 269 } 270 271 @Override 272 protected void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) { 273 if (token == QUERY_NEW_CALLS_TOKEN) { 274 int requestId = ((Integer) cookie).intValue(); 275 if (requestId != mCallsRequestId) { 276 // Ignore this query since it does not correspond to the latest request. 277 return; 278 } 279 280 // Store the returned cursor. 281 MoreCloseables.closeQuietly(mNewCallsCursor); 282 mNewCallsCursor = new ExtendedCursor( 283 cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM); 284 } else if (token == QUERY_OLD_CALLS_TOKEN) { 285 int requestId = ((Integer) cookie).intValue(); 286 if (requestId != mCallsRequestId) { 287 // Ignore this query since it does not correspond to the latest request. 288 return; 289 } 290 291 // Store the returned cursor. 292 MoreCloseables.closeQuietly(mOldCallsCursor); 293 mOldCallsCursor = new ExtendedCursor( 294 cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM); 295 } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) { 296 updateVoicemailStatus(cursor); 297 return; 298 } else { 299 Log.w(TAG, "Unknown query completed: ignoring: " + token); 300 return; 301 } 302 303 if (mNewCallsCursor != null && mOldCallsCursor != null) { 304 updateAdapterData(createMergedCursor()); 305 } 306 } 307 308 /** Creates the merged cursor representing the data to show in the call log. */ 309 @GuardedBy("this") 310 private Cursor createMergedCursor() { 311 try { 312 final boolean hasNewCalls = mNewCallsCursor.getCount() != 0; 313 final boolean hasOldCalls = mOldCallsCursor.getCount() != 0; 314 315 if (!hasNewCalls) { 316 // Return only the old calls, without the header. 317 MoreCloseables.closeQuietly(mNewCallsCursor); 318 return mOldCallsCursor; 319 } 320 321 if (!hasOldCalls) { 322 // Return only the new calls. 323 MoreCloseables.closeQuietly(mOldCallsCursor); 324 return new MergeCursor( 325 new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor }); 326 } 327 328 return new MergeCursor(new Cursor[]{ 329 createNewCallsHeaderCursor(), mNewCallsCursor, 330 createOldCallsHeaderCursor(), mOldCallsCursor}); 331 } finally { 332 // Any cursor still open is now owned, directly or indirectly, by the caller. 333 mNewCallsCursor = null; 334 mOldCallsCursor = null; 335 } 336 } 337 338 /** 339 * Updates the adapter in the call log fragment to show the new cursor data. 340 */ 341 private void updateAdapterData(Cursor combinedCursor) { 342 final Listener listener = mListener.get(); 343 if (listener != null) { 344 listener.onCallsFetched(combinedCursor); 345 } 346 } 347 348 private void updateVoicemailStatus(Cursor statusCursor) { 349 final Listener listener = mListener.get(); 350 if (listener != null) { 351 listener.onVoicemailStatusFetched(statusCursor); 352 } 353 } 354 355 /** Listener to completion of various queries. */ 356 public interface Listener { 357 /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */ 358 void onVoicemailStatusFetched(Cursor statusCursor); 359 360 /** 361 * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete. 362 */ 363 void onCallsFetched(Cursor combinedCursor); 364 } 365 } 366