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