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.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