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