Home | History | Annotate | Download | only in calllog
      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