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