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 45 /** Handles asynchronous queries to the call log. */ 46 public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { 47 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 48 49 private static final String TAG = "CallLogQueryHandler"; 50 private static final int NUM_LOGS_TO_DISPLAY = 1000; 51 52 /** The token for the query to fetch the old entries from the call log. */ 53 private static final int QUERY_CALLLOG_TOKEN = 54; 54 /** The token for the query to mark all missed calls as old after seeing the call log. */ 55 private static final int UPDATE_MARK_AS_OLD_TOKEN = 55; 56 /** The token for the query to mark all new voicemails as old. */ 57 private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56; 58 /** The token for the query to mark all missed calls as read after seeing the call log. */ 59 private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57; 60 /** The token for the query to fetch voicemail status messages. */ 61 private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58; 62 63 private final int mLogLimit; 64 65 /** 66 * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular 67 * type. 68 */ 69 public static final int CALL_TYPE_ALL = -1; 70 71 private final WeakReference<Listener> mListener; 72 73 /** 74 * Simple handler that wraps background calls to catch 75 * {@link SQLiteException}, such as when the disk is full. 76 */ 77 protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { CatchingWorkerHandler(Looper looper)78 public CatchingWorkerHandler(Looper looper) { 79 super(looper); 80 } 81 82 @Override handleMessage(Message msg)83 public void handleMessage(Message msg) { 84 try { 85 // Perform same query while catching any exceptions 86 super.handleMessage(msg); 87 } catch (SQLiteDiskIOException e) { 88 Log.w(TAG, "Exception on background worker thread", e); 89 } catch (SQLiteFullException e) { 90 Log.w(TAG, "Exception on background worker thread", e); 91 } catch (SQLiteDatabaseCorruptException e) { 92 Log.w(TAG, "Exception on background worker thread", e); 93 } catch (IllegalArgumentException e) { 94 Log.w(TAG, "ContactsProvider not present on device", e); 95 } 96 } 97 } 98 99 @Override createHandler(Looper looper)100 protected Handler createHandler(Looper looper) { 101 // Provide our special handler that catches exceptions 102 return new CatchingWorkerHandler(looper); 103 } 104 CallLogQueryHandler(ContentResolver contentResolver, Listener listener)105 public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) { 106 this(contentResolver, listener, -1); 107 } 108 CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit)109 public CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit) { 110 super(contentResolver); 111 mListener = new WeakReference<Listener>(listener); 112 mLogLimit = limit; 113 } 114 115 /** 116 * Fetches the list of calls from the call log for a given type. 117 * This call ignores the new or old state. 118 * <p> 119 * It will asynchronously update the content of the list view when the fetch completes. 120 */ fetchCalls(int callType, long newerThan)121 public void fetchCalls(int callType, long newerThan) { 122 cancelFetch(); 123 fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan); 124 } 125 fetchCalls(int callType)126 public void fetchCalls(int callType) { 127 fetchCalls(callType, 0); 128 } 129 fetchVoicemailStatus()130 public void fetchVoicemailStatus() { 131 startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI, 132 VoicemailStatusHelperImpl.PROJECTION, null, null, null); 133 } 134 135 /** Fetches the list of calls in the call log. */ fetchCalls(int token, int callType, boolean newOnly, long newerThan)136 private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) { 137 // We need to check for NULL explicitly otherwise entries with where READ is NULL 138 // may not match either the query or its negation. 139 // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new". 140 StringBuilder where = new StringBuilder(); 141 List<String> selectionArgs = Lists.newArrayList(); 142 143 if (newOnly) { 144 where.append(Calls.NEW); 145 where.append(" = 1"); 146 } 147 148 if (callType > CALL_TYPE_ALL) { 149 if (where.length() > 0) { 150 where.append(" AND "); 151 } 152 // Add a clause to fetch only items of type voicemail. 153 where.append(String.format("(%s = ?)", Calls.TYPE)); 154 // Add a clause to fetch only items newer than the requested date 155 selectionArgs.add(Integer.toString(callType)); 156 } 157 158 if (newerThan > 0) { 159 if (where.length() > 0) { 160 where.append(" AND "); 161 } 162 where.append(String.format("(%s > ?)", Calls.DATE)); 163 selectionArgs.add(Long.toString(newerThan)); 164 } 165 166 final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit; 167 final String selection = where.length() > 0 ? where.toString() : null; 168 Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon() 169 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit)) 170 .build(); 171 startQuery(token, null, uri, 172 CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY), 173 Calls.DEFAULT_SORT_ORDER); 174 } 175 176 /** Cancel any pending fetch request. */ cancelFetch()177 private void cancelFetch() { 178 cancelOperation(QUERY_CALLLOG_TOKEN); 179 } 180 181 /** Updates all new calls to mark them as old. */ markNewCallsAsOld()182 public void markNewCallsAsOld() { 183 // Mark all "new" calls as not new anymore. 184 StringBuilder where = new StringBuilder(); 185 where.append(Calls.NEW); 186 where.append(" = 1"); 187 188 ContentValues values = new ContentValues(1); 189 values.put(Calls.NEW, "0"); 190 191 startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 192 values, where.toString(), null); 193 } 194 195 /** Updates all new voicemails to mark them as old. */ markNewVoicemailsAsOld()196 public void markNewVoicemailsAsOld() { 197 // Mark all "new" voicemails as not new anymore. 198 StringBuilder where = new StringBuilder(); 199 where.append(Calls.NEW); 200 where.append(" = 1 AND "); 201 where.append(Calls.TYPE); 202 where.append(" = ?"); 203 204 ContentValues values = new ContentValues(1); 205 values.put(Calls.NEW, "0"); 206 207 startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 208 values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) }); 209 } 210 211 /** Updates all missed calls to mark them as read. */ markMissedCallsAsRead()212 public void markMissedCallsAsRead() { 213 // Mark all "new" calls as not new anymore. 214 StringBuilder where = new StringBuilder(); 215 where.append(Calls.IS_READ).append(" = 0"); 216 where.append(" AND "); 217 where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE); 218 219 ContentValues values = new ContentValues(1); 220 values.put(Calls.IS_READ, "1"); 221 222 startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values, 223 where.toString(), null); 224 } 225 226 @Override onNotNullableQueryComplete(int token, Object cookie, Cursor cursor)227 protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) { 228 if (cursor == null) { 229 return; 230 } 231 try { 232 if (token == QUERY_CALLLOG_TOKEN) { 233 if (updateAdapterData(cursor)) { 234 cursor = null; 235 } 236 } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) { 237 updateVoicemailStatus(cursor); 238 } else { 239 Log.w(TAG, "Unknown query completed: ignoring: " + token); 240 } 241 } finally { 242 if (cursor != null) { 243 cursor.close(); 244 } 245 } 246 } 247 248 /** 249 * Updates the adapter in the call log fragment to show the new cursor data. 250 * Returns true if the listener took ownership of the cursor. 251 */ updateAdapterData(Cursor cursor)252 private boolean updateAdapterData(Cursor cursor) { 253 final Listener listener = mListener.get(); 254 if (listener != null) { 255 return listener.onCallsFetched(cursor); 256 } 257 return false; 258 259 } 260 updateVoicemailStatus(Cursor statusCursor)261 private void updateVoicemailStatus(Cursor statusCursor) { 262 final Listener listener = mListener.get(); 263 if (listener != null) { 264 listener.onVoicemailStatusFetched(statusCursor); 265 } 266 } 267 268 /** Listener to completion of various queries. */ 269 public interface Listener { 270 /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */ onVoicemailStatusFetched(Cursor statusCursor)271 void onVoicemailStatusFetched(Cursor statusCursor); 272 273 /** 274 * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete. 275 * Returns true if takes ownership of cursor. 276 */ onCallsFetched(Cursor combinedCursor)277 boolean onCallsFetched(Cursor combinedCursor); 278 } 279 } 280