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