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.Build;
     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.provider.VoicemailContract.Voicemails;
     36 import android.util.Log;
     37 
     38 import com.android.contacts.common.compat.SdkVersionOverride;
     39 import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
     40 import com.android.contacts.common.util.PermissionsUtil;
     41 import com.android.dialer.database.VoicemailArchiveContract;
     42 import com.android.dialer.util.AppCompatConstants;
     43 import com.android.dialer.util.TelecomUtil;
     44 import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
     45 
     46 import com.google.common.collect.Lists;
     47 
     48 import java.lang.ref.WeakReference;
     49 import java.util.List;
     50 
     51 /** Handles asynchronous queries to the call log. */
     52 public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
     53     private static final String TAG = "CallLogQueryHandler";
     54     private static final int NUM_LOGS_TO_DISPLAY = 1000;
     55 
     56     /** The token for the query to fetch the old entries from the call log. */
     57     private static final int QUERY_CALLLOG_TOKEN = 54;
     58     /** The token for the query to mark all missed calls as old after seeing the call log. */
     59     private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
     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 = 56;
     62     /** The token for the query to fetch voicemail status messages. */
     63     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
     64     /** The token for the query to fetch the number of unread voicemails. */
     65     private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
     66     /** The token for the query to fetch the number of missed calls. */
     67     private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
     68     /** The oken for the query to fetch the archived voicemails. */
     69     private static final int QUERY_VOICEMAIL_ARCHIVE = 60;
     70 
     71     private final int mLogLimit;
     72 
     73     /**
     74      * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
     75      * type. Exception: excludes Calls.VOICEMAIL_TYPE.
     76      */
     77     public static final int CALL_TYPE_ALL = -1;
     78 
     79     private final WeakReference<Listener> mListener;
     80 
     81     private final Context mContext;
     82 
     83     /**
     84      * Simple handler that wraps background calls to catch
     85      * {@link SQLiteException}, such as when the disk is full.
     86      */
     87     protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
     88         public CatchingWorkerHandler(Looper looper) {
     89             super(looper);
     90         }
     91 
     92         @Override
     93         public void handleMessage(Message msg) {
     94             try {
     95                 // Perform same query while catching any exceptions
     96                 super.handleMessage(msg);
     97             } catch (SQLiteDiskIOException e) {
     98                 Log.w(TAG, "Exception on background worker thread", e);
     99             } catch (SQLiteFullException e) {
    100                 Log.w(TAG, "Exception on background worker thread", e);
    101             } catch (SQLiteDatabaseCorruptException e) {
    102                 Log.w(TAG, "Exception on background worker thread", e);
    103             } catch (IllegalArgumentException e) {
    104                 Log.w(TAG, "ContactsProvider not present on device", e);
    105             } catch (SecurityException e) {
    106                 // Shouldn't happen if we are protecting the entry points correctly,
    107                 // but just in case.
    108                 Log.w(TAG, "No permission to access ContactsProvider.", e);
    109             }
    110         }
    111     }
    112 
    113     @Override
    114     protected Handler createHandler(Looper looper) {
    115         // Provide our special handler that catches exceptions
    116         return new CatchingWorkerHandler(looper);
    117     }
    118 
    119     public CallLogQueryHandler(Context context, ContentResolver contentResolver,
    120             Listener listener) {
    121         this(context, contentResolver, listener, -1);
    122     }
    123 
    124     public CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener,
    125             int limit) {
    126         super(contentResolver);
    127         mContext = context.getApplicationContext();
    128         mListener = new WeakReference<Listener>(listener);
    129         mLogLimit = limit;
    130     }
    131 
    132     /**
    133      * Fetch all the voicemails in the voicemail archive.
    134      */
    135     public void fetchVoicemailArchive() {
    136         startQuery(QUERY_VOICEMAIL_ARCHIVE, null,
    137                 VoicemailArchiveContract.VoicemailArchive.CONTENT_URI,
    138                 null, VoicemailArchiveContract.VoicemailArchive.ARCHIVED + " = 1", null,
    139                 VoicemailArchiveContract.VoicemailArchive.DATE + " DESC");
    140     }
    141 
    142 
    143     /**
    144      * Fetches the list of calls from the call log for a given type.
    145      * This call ignores the new or old state.
    146      * <p>
    147      * It will asynchronously update the content of the list view when the fetch completes.
    148      */
    149     public void fetchCalls(int callType, long newerThan) {
    150         cancelFetch();
    151         if (PermissionsUtil.hasPhonePermissions(mContext)) {
    152             fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
    153         } else {
    154             updateAdapterData(null);
    155         }
    156     }
    157 
    158     public void fetchCalls(int callType) {
    159         fetchCalls(callType, 0);
    160     }
    161 
    162     public void fetchVoicemailStatus() {
    163         if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
    164             startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
    165                     VoicemailStatusHelperImpl.PROJECTION, null, null, null);
    166         }
    167     }
    168 
    169     public void fetchVoicemailUnreadCount() {
    170         if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
    171             // Only count voicemails that have not been read and have not been deleted.
    172             startQuery(QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN, null, Voicemails.CONTENT_URI,
    173                 new String[] { Voicemails._ID },
    174                     Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0", null, null);
    175         }
    176     }
    177 
    178     /** Fetches the list of calls in the call log. */
    179     private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
    180         StringBuilder where = new StringBuilder();
    181         List<String> selectionArgs = Lists.newArrayList();
    182 
    183         // Always hide blocked calls.
    184         where.append("(").append(Calls.TYPE).append(" != ?)");
    185         selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
    186 
    187         // Ignore voicemails marked as deleted
    188         if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
    189                 >= Build.VERSION_CODES.M) {
    190             where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
    191         }
    192 
    193         if (newOnly) {
    194             where.append(" AND (").append(Calls.NEW).append(" = 1)");
    195         }
    196 
    197         if (callType > CALL_TYPE_ALL) {
    198             where.append(" AND (").append(Calls.TYPE).append(" = ?)");
    199             selectionArgs.add(Integer.toString(callType));
    200         } else {
    201             where.append(" AND NOT ");
    202             where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
    203         }
    204 
    205         if (newerThan > 0) {
    206             where.append(" AND (").append(Calls.DATE).append(" > ?)");
    207             selectionArgs.add(Long.toString(newerThan));
    208         }
    209 
    210         final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
    211         final String selection = where.length() > 0 ? where.toString() : null;
    212         Uri uri = TelecomUtil.getCallLogUri(mContext).buildUpon()
    213                 .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
    214                 .build();
    215         startQuery(token, null, uri, CallLogQuery._PROJECTION, selection, selectionArgs.toArray(
    216                 new String[selectionArgs.size()]), Calls.DEFAULT_SORT_ORDER);
    217     }
    218 
    219     /** Cancel any pending fetch request. */
    220     private void cancelFetch() {
    221         cancelOperation(QUERY_CALLLOG_TOKEN);
    222     }
    223 
    224     /** Updates all new calls to mark them as old. */
    225     public void markNewCallsAsOld() {
    226         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
    227             return;
    228         }
    229         // Mark all "new" calls as not new anymore.
    230         StringBuilder where = new StringBuilder();
    231         where.append(Calls.NEW);
    232         where.append(" = 1");
    233 
    234         ContentValues values = new ContentValues(1);
    235         values.put(Calls.NEW, "0");
    236 
    237         startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, TelecomUtil.getCallLogUri(mContext),
    238                 values, where.toString(), null);
    239     }
    240 
    241     /** Updates all missed calls to mark them as read. */
    242     public void markMissedCallsAsRead() {
    243         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
    244             return;
    245         }
    246 
    247         ContentValues values = new ContentValues(1);
    248         values.put(Calls.IS_READ, "1");
    249 
    250         startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
    251                 getUnreadMissedCallsQuery(), null);
    252     }
    253 
    254     /** Fetch all missed calls received since last time the tab was opened. */
    255     public void fetchMissedCallsUnreadCount() {
    256         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
    257             return;
    258         }
    259 
    260         startQuery(QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN, null, Calls.CONTENT_URI,
    261                 new String[]{Calls._ID}, getUnreadMissedCallsQuery(), null, null);
    262     }
    263 
    264 
    265     @Override
    266     protected synchronized void onNotNullableQueryComplete(int token, Object cookie,
    267             Cursor cursor) {
    268         if (cursor == null) {
    269             return;
    270         }
    271         try {
    272             if (token == QUERY_CALLLOG_TOKEN || token == QUERY_VOICEMAIL_ARCHIVE) {
    273                 if (updateAdapterData(cursor)) {
    274                     cursor = null;
    275                 }
    276             } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
    277                 updateVoicemailStatus(cursor);
    278             } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
    279                 updateVoicemailUnreadCount(cursor);
    280             } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
    281                 updateMissedCallsUnreadCount(cursor);
    282             } else {
    283                 Log.w(TAG, "Unknown query completed: ignoring: " + token);
    284             }
    285         } finally {
    286             if (cursor != null) {
    287                 cursor.close();
    288             }
    289         }
    290     }
    291 
    292     /**
    293      * Updates the adapter in the call log fragment to show the new cursor data.
    294      * Returns true if the listener took ownership of the cursor.
    295      */
    296     private boolean updateAdapterData(Cursor cursor) {
    297         final Listener listener = mListener.get();
    298         if (listener != null) {
    299             return listener.onCallsFetched(cursor);
    300         }
    301         return false;
    302 
    303     }
    304 
    305     /**
    306      * @return Query string to get all unread missed calls.
    307      */
    308     private String getUnreadMissedCallsQuery() {
    309         StringBuilder where = new StringBuilder();
    310         where.append(Calls.IS_READ).append(" = 0 OR ").append(Calls.IS_READ).append(" IS NULL");
    311         where.append(" AND ");
    312         where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
    313         return where.toString();
    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     private void updateVoicemailUnreadCount(Cursor statusCursor) {
    324         final Listener listener = mListener.get();
    325         if (listener != null) {
    326             listener.onVoicemailUnreadCountFetched(statusCursor);
    327         }
    328     }
    329 
    330     private void updateMissedCallsUnreadCount(Cursor statusCursor) {
    331         final Listener listener = mListener.get();
    332         if (listener != null) {
    333             listener.onMissedCallsUnreadCountFetched(statusCursor);
    334         }
    335     }
    336 
    337     /** Listener to completion of various queries. */
    338     public interface Listener {
    339         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
    340         void onVoicemailStatusFetched(Cursor statusCursor);
    341 
    342         /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
    343         void onVoicemailUnreadCountFetched(Cursor cursor);
    344 
    345         /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
    346         void onMissedCallsUnreadCountFetched(Cursor cursor);
    347 
    348         /**
    349          * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete.
    350          * Returns true if takes ownership of cursor.
    351          */
    352         boolean onCallsFetched(Cursor combinedCursor);
    353     }
    354 }
    355