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