Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2007 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;
     18 
     19 import com.android.internal.telephony.CallerInfo;
     20 import com.android.internal.telephony.ITelephony;
     21 
     22 import android.app.AlertDialog;
     23 import android.app.Dialog;
     24 import android.app.ListActivity;
     25 import android.content.ActivityNotFoundException;
     26 import android.content.AsyncQueryHandler;
     27 import android.content.ContentUris;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.DialogInterface;
     31 import android.content.Intent;
     32 import android.content.DialogInterface.OnClickListener;
     33 import android.database.CharArrayBuffer;
     34 import android.database.Cursor;
     35 import android.database.sqlite.SQLiteDatabaseCorruptException;
     36 import android.database.sqlite.SQLiteDiskIOException;
     37 import android.database.sqlite.SQLiteException;
     38 import android.database.sqlite.SQLiteFullException;
     39 import android.graphics.drawable.Drawable;
     40 import android.net.Uri;
     41 import android.os.Bundle;
     42 import android.os.Handler;
     43 import android.os.Looper;
     44 import android.os.Message;
     45 import android.os.RemoteException;
     46 import android.os.ServiceManager;
     47 import android.os.SystemClock;
     48 import android.provider.CallLog;
     49 import android.provider.CallLog.Calls;
     50 import android.provider.ContactsContract.CommonDataKinds.Phone;
     51 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     52 import android.provider.ContactsContract.Contacts;
     53 import android.provider.ContactsContract.Data;
     54 import android.provider.ContactsContract.Intents.Insert;
     55 import android.provider.ContactsContract.PhoneLookup;
     56 import android.telephony.PhoneNumberUtils;
     57 import android.telephony.TelephonyManager;
     58 import android.text.SpannableStringBuilder;
     59 import android.text.TextUtils;
     60 import android.text.format.DateUtils;
     61 import android.util.Log;
     62 import android.view.ContextMenu;
     63 import android.view.KeyEvent;
     64 import android.view.LayoutInflater;
     65 import android.view.Menu;
     66 import android.view.MenuItem;
     67 import android.view.View;
     68 import android.view.ViewConfiguration;
     69 import android.view.ViewGroup;
     70 import android.view.ViewTreeObserver;
     71 import android.view.ContextMenu.ContextMenuInfo;
     72 import android.widget.AdapterView;
     73 import android.widget.ImageView;
     74 import android.widget.ListView;
     75 import android.widget.TextView;
     76 
     77 import java.lang.ref.WeakReference;
     78 import java.util.HashMap;
     79 import java.util.LinkedList;
     80 import java.util.Locale;
     81 
     82 /**
     83  * Displays a list of call log entries.
     84  */
     85 public class RecentCallsListActivity extends ListActivity
     86         implements View.OnCreateContextMenuListener {
     87     private static final String TAG = "RecentCallsList";
     88 
     89     /** The projection to use when querying the call log table */
     90     static final String[] CALL_LOG_PROJECTION = new String[] {
     91             Calls._ID,
     92             Calls.NUMBER,
     93             Calls.DATE,
     94             Calls.DURATION,
     95             Calls.TYPE,
     96             Calls.CACHED_NAME,
     97             Calls.CACHED_NUMBER_TYPE,
     98             Calls.CACHED_NUMBER_LABEL
     99     };
    100 
    101     static final int ID_COLUMN_INDEX = 0;
    102     static final int NUMBER_COLUMN_INDEX = 1;
    103     static final int DATE_COLUMN_INDEX = 2;
    104     static final int DURATION_COLUMN_INDEX = 3;
    105     static final int CALL_TYPE_COLUMN_INDEX = 4;
    106     static final int CALLER_NAME_COLUMN_INDEX = 5;
    107     static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 6;
    108     static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 7;
    109 
    110     /** The projection to use when querying the phones table */
    111     static final String[] PHONES_PROJECTION = new String[] {
    112             PhoneLookup._ID,
    113             PhoneLookup.DISPLAY_NAME,
    114             PhoneLookup.TYPE,
    115             PhoneLookup.LABEL,
    116             PhoneLookup.NUMBER
    117     };
    118 
    119     static final int PERSON_ID_COLUMN_INDEX = 0;
    120     static final int NAME_COLUMN_INDEX = 1;
    121     static final int PHONE_TYPE_COLUMN_INDEX = 2;
    122     static final int LABEL_COLUMN_INDEX = 3;
    123     static final int MATCHED_NUMBER_COLUMN_INDEX = 4;
    124 
    125     private static final int MENU_ITEM_DELETE_ALL = 1;
    126     private static final int CONTEXT_MENU_ITEM_DELETE = 1;
    127     private static final int CONTEXT_MENU_CALL_CONTACT = 2;
    128 
    129     private static final int QUERY_TOKEN = 53;
    130     private static final int UPDATE_TOKEN = 54;
    131 
    132     private static final int DIALOG_CONFIRM_DELETE_ALL = 1;
    133 
    134     RecentCallsAdapter mAdapter;
    135     private QueryHandler mQueryHandler;
    136     String mVoiceMailNumber;
    137 
    138     private boolean mScrollToTop;
    139 
    140     static final class ContactInfo {
    141         public long personId;
    142         public String name;
    143         public int type;
    144         public String label;
    145         public String number;
    146         public String formattedNumber;
    147 
    148         public static ContactInfo EMPTY = new ContactInfo();
    149     }
    150 
    151     public static final class RecentCallsListItemViews {
    152         TextView line1View;
    153         TextView labelView;
    154         TextView numberView;
    155         TextView dateView;
    156         ImageView iconView;
    157         View callView;
    158         ImageView groupIndicator;
    159         TextView groupSize;
    160     }
    161 
    162     static final class CallerInfoQuery {
    163         String number;
    164         int position;
    165         String name;
    166         int numberType;
    167         String numberLabel;
    168     }
    169 
    170     /**
    171      * Shared builder used by {@link #formatPhoneNumber(String)} to minimize
    172      * allocations when formatting phone numbers.
    173      */
    174     private static final SpannableStringBuilder sEditable = new SpannableStringBuilder();
    175 
    176     /**
    177      * Invalid formatting type constant for {@link #sFormattingType}.
    178      */
    179     private static final int FORMATTING_TYPE_INVALID = -1;
    180 
    181     /**
    182      * Cached formatting type for current {@link Locale}, as provided by
    183      * {@link PhoneNumberUtils#getFormatTypeForLocale(Locale)}.
    184      */
    185     private static int sFormattingType = FORMATTING_TYPE_INVALID;
    186 
    187     /** Adapter class to fill in data for the Call Log */
    188     final class RecentCallsAdapter extends GroupingListAdapter
    189             implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
    190         HashMap<String,ContactInfo> mContactInfo;
    191         private final LinkedList<CallerInfoQuery> mRequests;
    192         private volatile boolean mDone;
    193         private boolean mLoading = true;
    194         ViewTreeObserver.OnPreDrawListener mPreDrawListener;
    195         private static final int REDRAW = 1;
    196         private static final int START_THREAD = 2;
    197         private boolean mFirst;
    198         private Thread mCallerIdThread;
    199 
    200         private CharSequence[] mLabelArray;
    201 
    202         private Drawable mDrawableIncoming;
    203         private Drawable mDrawableOutgoing;
    204         private Drawable mDrawableMissed;
    205 
    206         /**
    207          * Reusable char array buffers.
    208          */
    209         private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
    210         private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
    211 
    212         public void onClick(View view) {
    213             String number = (String) view.getTag();
    214             if (!TextUtils.isEmpty(number)) {
    215                 // Here, "number" can either be a PSTN phone number or a
    216                 // SIP address.  So turn it into either a tel: URI or a
    217                 // sip: URI, as appropriate.
    218                 Uri callUri;
    219                 if (PhoneNumberUtils.isUriNumber(number)) {
    220                     callUri = Uri.fromParts("sip", number, null);
    221                 } else {
    222                     callUri = Uri.fromParts("tel", number, null);
    223                 }
    224                 StickyTabs.saveTab(RecentCallsListActivity.this, getIntent());
    225                 startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri));
    226             }
    227         }
    228 
    229         public boolean onPreDraw() {
    230             if (mFirst) {
    231                 mHandler.sendEmptyMessageDelayed(START_THREAD, 1000);
    232                 mFirst = false;
    233             }
    234             return true;
    235         }
    236 
    237         private Handler mHandler = new Handler() {
    238             @Override
    239             public void handleMessage(Message msg) {
    240                 switch (msg.what) {
    241                     case REDRAW:
    242                         notifyDataSetChanged();
    243                         break;
    244                     case START_THREAD:
    245                         startRequestProcessing();
    246                         break;
    247                 }
    248             }
    249         };
    250 
    251         public RecentCallsAdapter() {
    252             super(RecentCallsListActivity.this);
    253 
    254             mContactInfo = new HashMap<String,ContactInfo>();
    255             mRequests = new LinkedList<CallerInfoQuery>();
    256             mPreDrawListener = null;
    257 
    258             mDrawableIncoming = getResources().getDrawable(
    259                     R.drawable.ic_call_log_list_incoming_call);
    260             mDrawableOutgoing = getResources().getDrawable(
    261                     R.drawable.ic_call_log_list_outgoing_call);
    262             mDrawableMissed = getResources().getDrawable(
    263                     R.drawable.ic_call_log_list_missed_call);
    264             mLabelArray = getResources().getTextArray(com.android.internal.R.array.phoneTypes);
    265         }
    266 
    267         /**
    268          * Requery on background thread when {@link Cursor} changes.
    269          */
    270         @Override
    271         protected void onContentChanged() {
    272             // Start async requery
    273             startQuery();
    274         }
    275 
    276         void setLoading(boolean loading) {
    277             mLoading = loading;
    278         }
    279 
    280         @Override
    281         public boolean isEmpty() {
    282             if (mLoading) {
    283                 // We don't want the empty state to show when loading.
    284                 return false;
    285             } else {
    286                 return super.isEmpty();
    287             }
    288         }
    289 
    290         public ContactInfo getContactInfo(String number) {
    291             return mContactInfo.get(number);
    292         }
    293 
    294         public void startRequestProcessing() {
    295             mDone = false;
    296             mCallerIdThread = new Thread(this);
    297             mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
    298             mCallerIdThread.start();
    299         }
    300 
    301         public void stopRequestProcessing() {
    302             mDone = true;
    303             if (mCallerIdThread != null) mCallerIdThread.interrupt();
    304         }
    305 
    306         public void clearCache() {
    307             synchronized (mContactInfo) {
    308                 mContactInfo.clear();
    309             }
    310         }
    311 
    312         private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) {
    313             // Check if they are different. If not, don't update.
    314             if (TextUtils.equals(ciq.name, ci.name)
    315                     && TextUtils.equals(ciq.numberLabel, ci.label)
    316                     && ciq.numberType == ci.type) {
    317                 return;
    318             }
    319             ContentValues values = new ContentValues(3);
    320             values.put(Calls.CACHED_NAME, ci.name);
    321             values.put(Calls.CACHED_NUMBER_TYPE, ci.type);
    322             values.put(Calls.CACHED_NUMBER_LABEL, ci.label);
    323 
    324             try {
    325                 RecentCallsListActivity.this.getContentResolver().update(Calls.CONTENT_URI, values,
    326                         Calls.NUMBER + "='" + ciq.number + "'", null);
    327             } catch (SQLiteDiskIOException e) {
    328                 Log.w(TAG, "Exception while updating call info", e);
    329             } catch (SQLiteFullException e) {
    330                 Log.w(TAG, "Exception while updating call info", e);
    331             } catch (SQLiteDatabaseCorruptException e) {
    332                 Log.w(TAG, "Exception while updating call info", e);
    333             }
    334         }
    335 
    336         private void enqueueRequest(String number, int position,
    337                 String name, int numberType, String numberLabel) {
    338             CallerInfoQuery ciq = new CallerInfoQuery();
    339             ciq.number = number;
    340             ciq.position = position;
    341             ciq.name = name;
    342             ciq.numberType = numberType;
    343             ciq.numberLabel = numberLabel;
    344             synchronized (mRequests) {
    345                 mRequests.add(ciq);
    346                 mRequests.notifyAll();
    347             }
    348         }
    349 
    350         private boolean queryContactInfo(CallerInfoQuery ciq) {
    351             // First check if there was a prior request for the same number
    352             // that was already satisfied
    353             ContactInfo info = mContactInfo.get(ciq.number);
    354             boolean needNotify = false;
    355             if (info != null && info != ContactInfo.EMPTY) {
    356                 return true;
    357             } else {
    358                 // Ok, do a fresh Contacts lookup for ciq.number.
    359                 boolean infoUpdated = false;
    360 
    361                 if (PhoneNumberUtils.isUriNumber(ciq.number)) {
    362                     // This "number" is really a SIP address.
    363 
    364                     // TODO: This code is duplicated from the
    365                     // CallerInfoAsyncQuery class.  To avoid that, could the
    366                     // code here just use CallerInfoAsyncQuery, rather than
    367                     // manually running ContentResolver.query() itself?
    368 
    369                     // We look up SIP addresses directly in the Data table:
    370                     Uri contactRef = Data.CONTENT_URI;
    371 
    372                     // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
    373                     //
    374                     // Also note we use "upper(data1)" in the WHERE clause, and
    375                     // uppercase the incoming SIP address, in order to do a
    376                     // case-insensitive match.
    377                     //
    378                     // TODO: May also need to normalize by adding "sip:" as a
    379                     // prefix, if we start storing SIP addresses that way in the
    380                     // database.
    381                     String selection = "upper(" + Data.DATA1 + ")=?"
    382                             + " AND "
    383                             + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
    384                     String[] selectionArgs = new String[] { ciq.number.toUpperCase() };
    385 
    386                     Cursor dataTableCursor =
    387                             RecentCallsListActivity.this.getContentResolver().query(
    388                                     contactRef,
    389                                     null,  // projection
    390                                     selection,  // selection
    391                                     selectionArgs,  // selectionArgs
    392                                     null);  // sortOrder
    393 
    394                     if (dataTableCursor != null) {
    395                         if (dataTableCursor.moveToFirst()) {
    396                             info = new ContactInfo();
    397 
    398                             // TODO: we could slightly speed this up using an
    399                             // explicit projection (and thus not have to do
    400                             // those getColumnIndex() calls) but the benefit is
    401                             // very minimal.
    402 
    403                             // Note the Data.CONTACT_ID column here is
    404                             // equivalent to the PERSON_ID_COLUMN_INDEX column
    405                             // we use with "phonesCursor" below.
    406                             info.personId = dataTableCursor.getLong(
    407                                     dataTableCursor.getColumnIndex(Data.CONTACT_ID));
    408                             info.name = dataTableCursor.getString(
    409                                     dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
    410                             // "type" and "label" are currently unused for SIP addresses
    411                             info.type = SipAddress.TYPE_OTHER;
    412                             info.label = null;
    413 
    414                             // And "number" is the SIP address.
    415                             // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
    416                             info.number = dataTableCursor.getString(
    417                                     dataTableCursor.getColumnIndex(Data.DATA1));
    418 
    419                             infoUpdated = true;
    420                         }
    421                         dataTableCursor.close();
    422                     }
    423                 } else {
    424                     // "number" is a regular phone number, so use the
    425                     // PhoneLookup table:
    426                     Cursor phonesCursor =
    427                             RecentCallsListActivity.this.getContentResolver().query(
    428                                 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
    429                                                      Uri.encode(ciq.number)),
    430                                 PHONES_PROJECTION, null, null, null);
    431                     if (phonesCursor != null) {
    432                         if (phonesCursor.moveToFirst()) {
    433                             info = new ContactInfo();
    434                             info.personId = phonesCursor.getLong(PERSON_ID_COLUMN_INDEX);
    435                             info.name = phonesCursor.getString(NAME_COLUMN_INDEX);
    436                             info.type = phonesCursor.getInt(PHONE_TYPE_COLUMN_INDEX);
    437                             info.label = phonesCursor.getString(LABEL_COLUMN_INDEX);
    438                             info.number = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX);
    439 
    440                             infoUpdated = true;
    441                         }
    442                         phonesCursor.close();
    443                     }
    444                 }
    445 
    446                 if (infoUpdated) {
    447                     // New incoming phone number invalidates our formatted
    448                     // cache. Any cache fills happen only on the GUI thread.
    449                     info.formattedNumber = null;
    450 
    451                     mContactInfo.put(ciq.number, info);
    452 
    453                     // Inform list to update this item, if in view
    454                     needNotify = true;
    455                 }
    456             }
    457             if (info != null) {
    458                 updateCallLog(ciq, info);
    459             }
    460             return needNotify;
    461         }
    462 
    463         /*
    464          * Handles requests for contact name and number type
    465          * @see java.lang.Runnable#run()
    466          */
    467         public void run() {
    468             boolean needNotify = false;
    469             while (!mDone) {
    470                 CallerInfoQuery ciq = null;
    471                 synchronized (mRequests) {
    472                     if (!mRequests.isEmpty()) {
    473                         ciq = mRequests.removeFirst();
    474                     } else {
    475                         if (needNotify) {
    476                             needNotify = false;
    477                             mHandler.sendEmptyMessage(REDRAW);
    478                         }
    479                         try {
    480                             mRequests.wait(1000);
    481                         } catch (InterruptedException ie) {
    482                             // Ignore and continue processing requests
    483                         }
    484                     }
    485                 }
    486                 if (ciq != null && queryContactInfo(ciq)) {
    487                     needNotify = true;
    488                 }
    489             }
    490         }
    491 
    492         @Override
    493         protected void addGroups(Cursor cursor) {
    494 
    495             int count = cursor.getCount();
    496             if (count == 0) {
    497                 return;
    498             }
    499 
    500             int groupItemCount = 1;
    501 
    502             CharArrayBuffer currentValue = mBuffer1;
    503             CharArrayBuffer value = mBuffer2;
    504             cursor.moveToFirst();
    505             cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, currentValue);
    506             int currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
    507             for (int i = 1; i < count; i++) {
    508                 cursor.moveToNext();
    509                 cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, value);
    510                 boolean sameNumber = equalPhoneNumbers(value, currentValue);
    511 
    512                 // Group adjacent calls with the same number. Make an exception
    513                 // for the latest item if it was a missed call.  We don't want
    514                 // a missed call to be hidden inside a group.
    515                 if (sameNumber && currentCallType != Calls.MISSED_TYPE) {
    516                     groupItemCount++;
    517                 } else {
    518                     if (groupItemCount > 1) {
    519                         addGroup(i - groupItemCount, groupItemCount, false);
    520                     }
    521 
    522                     groupItemCount = 1;
    523 
    524                     // Swap buffers
    525                     CharArrayBuffer temp = currentValue;
    526                     currentValue = value;
    527                     value = temp;
    528 
    529                     // If we have just examined a row following a missed call, make
    530                     // sure that it is grouped with subsequent calls from the same number
    531                     // even if it was also missed.
    532                     if (sameNumber && currentCallType == Calls.MISSED_TYPE) {
    533                         currentCallType = 0;       // "not a missed call"
    534                     } else {
    535                         currentCallType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
    536                     }
    537                 }
    538             }
    539             if (groupItemCount > 1) {
    540                 addGroup(count - groupItemCount, groupItemCount, false);
    541             }
    542         }
    543 
    544         protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
    545 
    546             // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
    547             // string allocation
    548             return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied),
    549                     new String(buffer2.data, 0, buffer2.sizeCopied));
    550         }
    551 
    552 
    553         @Override
    554         protected View newStandAloneView(Context context, ViewGroup parent) {
    555             LayoutInflater inflater =
    556                     (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    557             View view = inflater.inflate(R.layout.recent_calls_list_item, parent, false);
    558             findAndCacheViews(view);
    559             return view;
    560         }
    561 
    562         @Override
    563         protected void bindStandAloneView(View view, Context context, Cursor cursor) {
    564             bindView(context, view, cursor);
    565         }
    566 
    567         @Override
    568         protected View newChildView(Context context, ViewGroup parent) {
    569             LayoutInflater inflater =
    570                     (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    571             View view = inflater.inflate(R.layout.recent_calls_list_child_item, parent, false);
    572             findAndCacheViews(view);
    573             return view;
    574         }
    575 
    576         @Override
    577         protected void bindChildView(View view, Context context, Cursor cursor) {
    578             bindView(context, view, cursor);
    579         }
    580 
    581         @Override
    582         protected View newGroupView(Context context, ViewGroup parent) {
    583             LayoutInflater inflater =
    584                     (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    585             View view = inflater.inflate(R.layout.recent_calls_list_group_item, parent, false);
    586             findAndCacheViews(view);
    587             return view;
    588         }
    589 
    590         @Override
    591         protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    592                 boolean expanded) {
    593             final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag();
    594             int groupIndicator = expanded
    595                     ? com.android.internal.R.drawable.expander_ic_maximized
    596                     : com.android.internal.R.drawable.expander_ic_minimized;
    597             views.groupIndicator.setImageResource(groupIndicator);
    598             views.groupSize.setText("(" + groupSize + ")");
    599             bindView(context, view, cursor);
    600         }
    601 
    602         private void findAndCacheViews(View view) {
    603 
    604             // Get the views to bind to
    605             RecentCallsListItemViews views = new RecentCallsListItemViews();
    606             views.line1View = (TextView) view.findViewById(R.id.line1);
    607             views.labelView = (TextView) view.findViewById(R.id.label);
    608             views.numberView = (TextView) view.findViewById(R.id.number);
    609             views.dateView = (TextView) view.findViewById(R.id.date);
    610             views.iconView = (ImageView) view.findViewById(R.id.call_type_icon);
    611             views.callView = view.findViewById(R.id.call_icon);
    612             views.callView.setOnClickListener(this);
    613             views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator);
    614             views.groupSize = (TextView) view.findViewById(R.id.groupSize);
    615             view.setTag(views);
    616         }
    617 
    618         public void bindView(Context context, View view, Cursor c) {
    619             final RecentCallsListItemViews views = (RecentCallsListItemViews) view.getTag();
    620 
    621             String number = c.getString(NUMBER_COLUMN_INDEX);
    622             String formattedNumber = null;
    623             String callerName = c.getString(CALLER_NAME_COLUMN_INDEX);
    624             int callerNumberType = c.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
    625             String callerNumberLabel = c.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
    626 
    627             // Store away the number so we can call it directly if you click on the call icon
    628             views.callView.setTag(number);
    629 
    630             // Lookup contacts with this number
    631             ContactInfo info = mContactInfo.get(number);
    632             if (info == null) {
    633                 // Mark it as empty and queue up a request to find the name
    634                 // The db request should happen on a non-UI thread
    635                 info = ContactInfo.EMPTY;
    636                 mContactInfo.put(number, info);
    637                 enqueueRequest(number, c.getPosition(),
    638                         callerName, callerNumberType, callerNumberLabel);
    639             } else if (info != ContactInfo.EMPTY) { // Has been queried
    640                 // Check if any data is different from the data cached in the
    641                 // calls db. If so, queue the request so that we can update
    642                 // the calls db.
    643                 if (!TextUtils.equals(info.name, callerName)
    644                         || info.type != callerNumberType
    645                         || !TextUtils.equals(info.label, callerNumberLabel)) {
    646                     // Something is amiss, so sync up.
    647                     enqueueRequest(number, c.getPosition(),
    648                             callerName, callerNumberType, callerNumberLabel);
    649                 }
    650 
    651                 // Format and cache phone number for found contact
    652                 if (info.formattedNumber == null) {
    653                     info.formattedNumber = formatPhoneNumber(info.number);
    654                 }
    655                 formattedNumber = info.formattedNumber;
    656             }
    657 
    658             String name = info.name;
    659             int ntype = info.type;
    660             String label = info.label;
    661             // If there's no name cached in our hashmap, but there's one in the
    662             // calls db, use the one in the calls db. Otherwise the name in our
    663             // hashmap is more recent, so it has precedence.
    664             if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) {
    665                 name = callerName;
    666                 ntype = callerNumberType;
    667                 label = callerNumberLabel;
    668 
    669                 // Format the cached call_log phone number
    670                 formattedNumber = formatPhoneNumber(number);
    671             }
    672             // Set the text lines and call icon.
    673             // Assumes the call back feature is on most of the
    674             // time. For private and unknown numbers: hide it.
    675             views.callView.setVisibility(View.VISIBLE);
    676 
    677             if (!TextUtils.isEmpty(name)) {
    678                 views.line1View.setText(name);
    679                 views.labelView.setVisibility(View.VISIBLE);
    680 
    681                 // "type" and "label" are currently unused for SIP addresses.
    682                 CharSequence numberLabel = null;
    683                 if (!PhoneNumberUtils.isUriNumber(number)) {
    684                     numberLabel = Phone.getDisplayLabel(context, ntype, label,
    685                             mLabelArray);
    686                 }
    687                 views.numberView.setVisibility(View.VISIBLE);
    688                 views.numberView.setText(formattedNumber);
    689                 if (!TextUtils.isEmpty(numberLabel)) {
    690                     views.labelView.setText(numberLabel);
    691                     views.labelView.setVisibility(View.VISIBLE);
    692 
    693                     // Zero out the numberView's left margin (see below)
    694                     ViewGroup.MarginLayoutParams numberLP =
    695                             (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
    696                     numberLP.leftMargin = 0;
    697                     views.numberView.setLayoutParams(numberLP);
    698                 } else {
    699                     // There's nothing to display in views.labelView, so hide it.
    700                     // We can't set it to View.GONE, since it's the anchor for
    701                     // numberView in the RelativeLayout, so make it INVISIBLE.
    702                     //   Also, we need to manually *subtract* some left margin from
    703                     // numberView to compensate for the right margin built in to
    704                     // labelView (otherwise the number will be indented by a very
    705                     // slight amount).
    706                     //   TODO: a cleaner fix would be to contain both the label and
    707                     // number inside a LinearLayout, and then set labelView *and*
    708                     // its padding to GONE when there's no label to display.
    709                     views.labelView.setText(null);
    710                     views.labelView.setVisibility(View.INVISIBLE);
    711 
    712                     ViewGroup.MarginLayoutParams labelLP =
    713                             (ViewGroup.MarginLayoutParams) views.labelView.getLayoutParams();
    714                     ViewGroup.MarginLayoutParams numberLP =
    715                             (ViewGroup.MarginLayoutParams) views.numberView.getLayoutParams();
    716                     // Equivalent to setting android:layout_marginLeft in XML
    717                     numberLP.leftMargin = -labelLP.rightMargin;
    718                     views.numberView.setLayoutParams(numberLP);
    719                 }
    720             } else {
    721                 if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
    722                     number = getString(R.string.unknown);
    723                     views.callView.setVisibility(View.INVISIBLE);
    724                 } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
    725                     number = getString(R.string.private_num);
    726                     views.callView.setVisibility(View.INVISIBLE);
    727                 } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
    728                     number = getString(R.string.payphone);
    729                 } else if (PhoneNumberUtils.extractNetworkPortion(number)
    730                                 .equals(mVoiceMailNumber)) {
    731                     number = getString(R.string.voicemail);
    732                 } else {
    733                     // Just a raw number, and no cache, so format it nicely
    734                     number = formatPhoneNumber(number);
    735                 }
    736 
    737                 views.line1View.setText(number);
    738                 views.numberView.setVisibility(View.GONE);
    739                 views.labelView.setVisibility(View.GONE);
    740             }
    741 
    742             long date = c.getLong(DATE_COLUMN_INDEX);
    743 
    744             // Set the date/time field by mixing relative and absolute times.
    745             int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
    746 
    747             views.dateView.setText(DateUtils.getRelativeTimeSpanString(date,
    748                     System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags));
    749 
    750             if (views.iconView != null) {
    751                 int type = c.getInt(CALL_TYPE_COLUMN_INDEX);
    752                 // Set the icon
    753                 switch (type) {
    754                     case Calls.INCOMING_TYPE:
    755                         views.iconView.setImageDrawable(mDrawableIncoming);
    756                         break;
    757 
    758                     case Calls.OUTGOING_TYPE:
    759                         views.iconView.setImageDrawable(mDrawableOutgoing);
    760                         break;
    761 
    762                     case Calls.MISSED_TYPE:
    763                         views.iconView.setImageDrawable(mDrawableMissed);
    764                         break;
    765                 }
    766             }
    767 
    768             // Listen for the first draw
    769             if (mPreDrawListener == null) {
    770                 mFirst = true;
    771                 mPreDrawListener = this;
    772                 view.getViewTreeObserver().addOnPreDrawListener(this);
    773             }
    774         }
    775     }
    776 
    777     private static final class QueryHandler extends AsyncQueryHandler {
    778         private final WeakReference<RecentCallsListActivity> mActivity;
    779 
    780         /**
    781          * Simple handler that wraps background calls to catch
    782          * {@link SQLiteException}, such as when the disk is full.
    783          */
    784         protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
    785             public CatchingWorkerHandler(Looper looper) {
    786                 super(looper);
    787             }
    788 
    789             @Override
    790             public void handleMessage(Message msg) {
    791                 try {
    792                     // Perform same query while catching any exceptions
    793                     super.handleMessage(msg);
    794                 } catch (SQLiteDiskIOException e) {
    795                     Log.w(TAG, "Exception on background worker thread", e);
    796                 } catch (SQLiteFullException e) {
    797                     Log.w(TAG, "Exception on background worker thread", e);
    798                 } catch (SQLiteDatabaseCorruptException e) {
    799                     Log.w(TAG, "Exception on background worker thread", e);
    800                 }
    801             }
    802         }
    803 
    804         @Override
    805         protected Handler createHandler(Looper looper) {
    806             // Provide our special handler that catches exceptions
    807             return new CatchingWorkerHandler(looper);
    808         }
    809 
    810         public QueryHandler(Context context) {
    811             super(context.getContentResolver());
    812             mActivity = new WeakReference<RecentCallsListActivity>(
    813                     (RecentCallsListActivity) context);
    814         }
    815 
    816         @Override
    817         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    818             final RecentCallsListActivity activity = mActivity.get();
    819             if (activity != null && !activity.isFinishing()) {
    820                 final RecentCallsListActivity.RecentCallsAdapter callsAdapter = activity.mAdapter;
    821                 callsAdapter.setLoading(false);
    822                 callsAdapter.changeCursor(cursor);
    823                 if (activity.mScrollToTop) {
    824                     if (activity.mList.getFirstVisiblePosition() > 5) {
    825                         activity.mList.setSelection(5);
    826                     }
    827                     activity.mList.smoothScrollToPosition(0);
    828                     activity.mScrollToTop = false;
    829                 }
    830             } else {
    831                 cursor.close();
    832             }
    833         }
    834     }
    835 
    836     @Override
    837     protected void onCreate(Bundle state) {
    838         super.onCreate(state);
    839 
    840         setContentView(R.layout.recent_calls);
    841 
    842         // Typing here goes to the dialer
    843         setDefaultKeyMode(DEFAULT_KEYS_DIALER);
    844 
    845         mAdapter = new RecentCallsAdapter();
    846         getListView().setOnCreateContextMenuListener(this);
    847         setListAdapter(mAdapter);
    848 
    849         mVoiceMailNumber = ((TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE))
    850                 .getVoiceMailNumber();
    851         mQueryHandler = new QueryHandler(this);
    852 
    853         // Reset locale-based formatting cache
    854         sFormattingType = FORMATTING_TYPE_INVALID;
    855     }
    856 
    857     @Override
    858     protected void onStart() {
    859         mScrollToTop = true;
    860         super.onStart();
    861     }
    862 
    863     @Override
    864     protected void onResume() {
    865         // The adapter caches looked up numbers, clear it so they will get
    866         // looked up again.
    867         if (mAdapter != null) {
    868             mAdapter.clearCache();
    869         }
    870 
    871         startQuery();
    872         resetNewCallsFlag();
    873 
    874         super.onResume();
    875 
    876         mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
    877     }
    878 
    879     @Override
    880     protected void onPause() {
    881         super.onPause();
    882 
    883         // Kill the requests thread
    884         mAdapter.stopRequestProcessing();
    885     }
    886 
    887     @Override
    888     protected void onDestroy() {
    889         super.onDestroy();
    890         mAdapter.stopRequestProcessing();
    891         mAdapter.changeCursor(null);
    892     }
    893 
    894     @Override
    895     public void onWindowFocusChanged(boolean hasFocus) {
    896         super.onWindowFocusChanged(hasFocus);
    897 
    898         // Clear notifications only when window gains focus.  This activity won't
    899         // immediately receive focus if the keyguard screen is above it.
    900         if (hasFocus) {
    901             try {
    902                 ITelephony iTelephony =
    903                         ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
    904                 if (iTelephony != null) {
    905                     iTelephony.cancelMissedCallsNotification();
    906                 } else {
    907                     Log.w(TAG, "Telephony service is null, can't call " +
    908                             "cancelMissedCallsNotification");
    909                 }
    910             } catch (RemoteException e) {
    911                 Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
    912             }
    913         }
    914     }
    915 
    916     /**
    917      * Format the given phone number using
    918      * {@link PhoneNumberUtils#formatNumber(android.text.Editable, int)}. This
    919      * helper method uses {@link #sEditable} and {@link #sFormattingType} to
    920      * prevent allocations between multiple calls.
    921      * <p>
    922      * Because of the shared {@link #sEditable} builder, <b>this method is not
    923      * thread safe</b>, and should only be called from the GUI thread.
    924      * <p>
    925      * If the given String object is null or empty, return an empty String.
    926      */
    927     private String formatPhoneNumber(String number) {
    928         if (TextUtils.isEmpty(number)) {
    929             return "";
    930         }
    931 
    932         // If "number" is really a SIP address, don't try to do any formatting at all.
    933         if (PhoneNumberUtils.isUriNumber(number)) {
    934             return number;
    935         }
    936 
    937         // Cache formatting type if not already present
    938         if (sFormattingType == FORMATTING_TYPE_INVALID) {
    939             sFormattingType = PhoneNumberUtils.getFormatTypeForLocale(Locale.getDefault());
    940         }
    941 
    942         sEditable.clear();
    943         sEditable.append(number);
    944 
    945         PhoneNumberUtils.formatNumber(sEditable, sFormattingType);
    946         return sEditable.toString();
    947     }
    948 
    949     private void resetNewCallsFlag() {
    950         // Mark all "new" missed calls as not new anymore
    951         StringBuilder where = new StringBuilder("type=");
    952         where.append(Calls.MISSED_TYPE);
    953         where.append(" AND new=1");
    954 
    955         ContentValues values = new ContentValues(1);
    956         values.put(Calls.NEW, "0");
    957         mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI,
    958                 values, where.toString(), null);
    959     }
    960 
    961     private void startQuery() {
    962         mAdapter.setLoading(true);
    963 
    964         // Cancel any pending queries
    965         mQueryHandler.cancelOperation(QUERY_TOKEN);
    966         mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI,
    967                 CALL_LOG_PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
    968     }
    969 
    970     @Override
    971     public boolean onCreateOptionsMenu(Menu menu) {
    972         menu.add(0, MENU_ITEM_DELETE_ALL, 0, R.string.recentCalls_deleteAll)
    973                 .setIcon(android.R.drawable.ic_menu_close_clear_cancel);
    974         return true;
    975     }
    976 
    977     @Override
    978     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
    979         AdapterView.AdapterContextMenuInfo menuInfo;
    980         try {
    981              menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn;
    982         } catch (ClassCastException e) {
    983             Log.e(TAG, "bad menuInfoIn", e);
    984             return;
    985         }
    986 
    987         Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position);
    988 
    989         String number = cursor.getString(NUMBER_COLUMN_INDEX);
    990         Uri numberUri = null;
    991         boolean isVoicemail = false;
    992         boolean isSipNumber = false;
    993         if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
    994             number = getString(R.string.unknown);
    995         } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
    996             number = getString(R.string.private_num);
    997         } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
    998             number = getString(R.string.payphone);
    999         } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) {
   1000             number = getString(R.string.voicemail);
   1001             numberUri = Uri.parse("voicemail:x");
   1002             isVoicemail = true;
   1003         } else if (PhoneNumberUtils.isUriNumber(number)) {
   1004             numberUri = Uri.fromParts("sip", number, null);
   1005             isSipNumber = true;
   1006         } else {
   1007             numberUri = Uri.fromParts("tel", number, null);
   1008         }
   1009 
   1010         ContactInfo info = mAdapter.getContactInfo(number);
   1011         boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY);
   1012         if (contactInfoPresent) {
   1013             menu.setHeaderTitle(info.name);
   1014         } else {
   1015             menu.setHeaderTitle(number);
   1016         }
   1017 
   1018         if (numberUri != null) {
   1019             Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri);
   1020             menu.add(0, CONTEXT_MENU_CALL_CONTACT, 0,
   1021                     getResources().getString(R.string.recentCalls_callNumber, number))
   1022                     .setIntent(intent);
   1023         }
   1024 
   1025         if (contactInfoPresent) {
   1026             Intent intent = new Intent(Intent.ACTION_VIEW,
   1027                     ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId));
   1028             StickyTabs.setTab(intent, getIntent());
   1029             menu.add(0, 0, 0, R.string.menu_viewContact).setIntent(intent);
   1030         }
   1031 
   1032         if (numberUri != null && !isVoicemail && !isSipNumber) {
   1033             menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall)
   1034                     .setIntent(new Intent(Intent.ACTION_DIAL, numberUri));
   1035             menu.add(0, 0, 0, R.string.menu_sendTextMessage)
   1036                     .setIntent(new Intent(Intent.ACTION_SENDTO,
   1037                             Uri.fromParts("sms", number, null)));
   1038         }
   1039 
   1040         // "Add to contacts" item, if this entry isn't already associated with a contact
   1041         if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) {
   1042             // TODO: This item is currently disabled for SIP addresses, because
   1043             // the Insert.PHONE extra only works correctly for PSTN numbers.
   1044             //
   1045             // To fix this for SIP addresses, we need to:
   1046             // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
   1047             //   the current number is a SIP address
   1048             // - update the contacts UI code to handle Insert.SIP_ADDRESS by
   1049             //   updating the SipAddress field
   1050             // and then we can remove the "!isSipNumber" check above.
   1051 
   1052             Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
   1053             intent.setType(Contacts.CONTENT_ITEM_TYPE);
   1054             intent.putExtra(Insert.PHONE, number);
   1055             menu.add(0, 0, 0, R.string.recentCalls_addToContact)
   1056                     .setIntent(intent);
   1057         }
   1058         menu.add(0, CONTEXT_MENU_ITEM_DELETE, 0, R.string.recentCalls_removeFromRecentList);
   1059     }
   1060 
   1061     @Override
   1062     protected Dialog onCreateDialog(int id, Bundle args) {
   1063         switch (id) {
   1064             case DIALOG_CONFIRM_DELETE_ALL:
   1065                 return new AlertDialog.Builder(this)
   1066                     .setTitle(R.string.clearCallLogConfirmation_title)
   1067                     .setIcon(android.R.drawable.ic_dialog_alert)
   1068                     .setMessage(R.string.clearCallLogConfirmation)
   1069                     .setNegativeButton(android.R.string.cancel, null)
   1070                     .setPositiveButton(android.R.string.ok, new OnClickListener() {
   1071                         public void onClick(DialogInterface dialog, int which) {
   1072                             getContentResolver().delete(Calls.CONTENT_URI, null, null);
   1073                             // TODO The change notification should do this automatically, but it
   1074                             // isn't working right now. Remove this when the change notification
   1075                             // is working properly.
   1076                             startQuery();
   1077                         }
   1078                     })
   1079                     .setCancelable(false)
   1080                     .create();
   1081         }
   1082         return null;
   1083     }
   1084 
   1085     @Override
   1086     public boolean onOptionsItemSelected(MenuItem item) {
   1087         switch (item.getItemId()) {
   1088             case MENU_ITEM_DELETE_ALL: {
   1089                 showDialog(DIALOG_CONFIRM_DELETE_ALL);
   1090                 return true;
   1091             }
   1092         }
   1093         return super.onOptionsItemSelected(item);
   1094     }
   1095 
   1096     @Override
   1097     public boolean onContextItemSelected(MenuItem item) {
   1098         switch (item.getItemId()) {
   1099             case CONTEXT_MENU_ITEM_DELETE: {
   1100                 // Convert the menu info to the proper type
   1101                 AdapterView.AdapterContextMenuInfo menuInfo;
   1102                 try {
   1103                      menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
   1104                 } catch (ClassCastException e) {
   1105                     Log.e(TAG, "bad menuInfoIn", e);
   1106                     return false;
   1107                 }
   1108 
   1109                 Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position);
   1110                 int groupSize = 1;
   1111                 if (mAdapter.isGroupHeader(menuInfo.position)) {
   1112                     groupSize = mAdapter.getGroupSize(menuInfo.position);
   1113                 }
   1114 
   1115                 StringBuilder sb = new StringBuilder();
   1116                 for (int i = 0; i < groupSize; i++) {
   1117                     if (i != 0) {
   1118                         sb.append(",");
   1119                         cursor.moveToNext();
   1120                     }
   1121                     long id = cursor.getLong(ID_COLUMN_INDEX);
   1122                     sb.append(id);
   1123                 }
   1124 
   1125                 getContentResolver().delete(Calls.CONTENT_URI, Calls._ID + " IN (" + sb + ")",
   1126                         null);
   1127                 return true;
   1128             }
   1129             case CONTEXT_MENU_CALL_CONTACT: {
   1130                 StickyTabs.saveTab(this, getIntent());
   1131                 startActivity(item.getIntent());
   1132                 return true;
   1133             }
   1134             default: {
   1135                 return super.onContextItemSelected(item);
   1136             }
   1137         }
   1138     }
   1139 
   1140     @Override
   1141     public boolean onKeyDown(int keyCode, KeyEvent event) {
   1142         switch (keyCode) {
   1143             case KeyEvent.KEYCODE_CALL: {
   1144                 long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
   1145                 if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
   1146                     // Launch voice dialer
   1147                     Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
   1148                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1149                     try {
   1150                         startActivity(intent);
   1151                     } catch (ActivityNotFoundException e) {
   1152                     }
   1153                     return true;
   1154                 }
   1155             }
   1156         }
   1157         return super.onKeyDown(keyCode, event);
   1158     }
   1159 
   1160     @Override
   1161     public boolean onKeyUp(int keyCode, KeyEvent event) {
   1162         switch (keyCode) {
   1163             case KeyEvent.KEYCODE_CALL:
   1164                 try {
   1165                     ITelephony phone = ITelephony.Stub.asInterface(
   1166                             ServiceManager.checkService("phone"));
   1167                     if (phone != null && !phone.isIdle()) {
   1168                         // Let the super class handle it
   1169                         break;
   1170                     }
   1171                 } catch (RemoteException re) {
   1172                     // Fall through and try to call the contact
   1173                 }
   1174 
   1175                 callEntry(getListView().getSelectedItemPosition());
   1176                 return true;
   1177         }
   1178         return super.onKeyUp(keyCode, event);
   1179     }
   1180 
   1181     /*
   1182      * Get the number from the Contacts, if available, since sometimes
   1183      * the number provided by caller id may not be formatted properly
   1184      * depending on the carrier (roaming) in use at the time of the
   1185      * incoming call.
   1186      * Logic : If the caller-id number starts with a "+", use it
   1187      *         Else if the number in the contacts starts with a "+", use that one
   1188      *         Else if the number in the contacts is longer, use that one
   1189      */
   1190     private String getBetterNumberFromContacts(String number) {
   1191         String matchingNumber = null;
   1192         // Look in the cache first. If it's not found then query the Phones db
   1193         ContactInfo ci = mAdapter.mContactInfo.get(number);
   1194         if (ci != null && ci != ContactInfo.EMPTY) {
   1195             matchingNumber = ci.number;
   1196         } else {
   1197             try {
   1198                 Cursor phonesCursor =
   1199                     RecentCallsListActivity.this.getContentResolver().query(
   1200                             Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
   1201                                     number),
   1202                     PHONES_PROJECTION, null, null, null);
   1203                 if (phonesCursor != null) {
   1204                     if (phonesCursor.moveToFirst()) {
   1205                         matchingNumber = phonesCursor.getString(MATCHED_NUMBER_COLUMN_INDEX);
   1206                     }
   1207                     phonesCursor.close();
   1208                 }
   1209             } catch (Exception e) {
   1210                 // Use the number from the call log
   1211             }
   1212         }
   1213         if (!TextUtils.isEmpty(matchingNumber) &&
   1214                 (matchingNumber.startsWith("+")
   1215                         || matchingNumber.length() > number.length())) {
   1216             number = matchingNumber;
   1217         }
   1218         return number;
   1219     }
   1220 
   1221     private void callEntry(int position) {
   1222         if (position < 0) {
   1223             // In touch mode you may often not have something selected, so
   1224             // just call the first entry to make sure that [send] [send] calls the
   1225             // most recent entry.
   1226             position = 0;
   1227         }
   1228         final Cursor cursor = (Cursor)mAdapter.getItem(position);
   1229         if (cursor != null) {
   1230             String number = cursor.getString(NUMBER_COLUMN_INDEX);
   1231             if (TextUtils.isEmpty(number)
   1232                     || number.equals(CallerInfo.UNKNOWN_NUMBER)
   1233                     || number.equals(CallerInfo.PRIVATE_NUMBER)
   1234                     || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
   1235                 // This number can't be called, do nothing
   1236                 return;
   1237             }
   1238             Intent intent;
   1239             // If "number" is really a SIP address, construct a sip: URI.
   1240             if (PhoneNumberUtils.isUriNumber(number)) {
   1241                 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
   1242                                     Uri.fromParts("sip", number, null));
   1243             } else {
   1244                 // We're calling a regular PSTN phone number.
   1245                 // Construct a tel: URI, but do some other possible cleanup first.
   1246                 int callType = cursor.getInt(CALL_TYPE_COLUMN_INDEX);
   1247                 if (!number.startsWith("+") &&
   1248                        (callType == Calls.INCOMING_TYPE
   1249                                 || callType == Calls.MISSED_TYPE)) {
   1250                     // If the caller-id matches a contact with a better qualified number, use it
   1251                     number = getBetterNumberFromContacts(number);
   1252                 }
   1253                 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
   1254                                     Uri.fromParts("tel", number, null));
   1255             }
   1256             StickyTabs.saveTab(this, getIntent());
   1257             intent.setFlags(
   1258                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
   1259             startActivity(intent);
   1260         }
   1261     }
   1262 
   1263     @Override
   1264     protected void onListItemClick(ListView l, View v, int position, long id) {
   1265         if (mAdapter.isGroupHeader(position)) {
   1266             mAdapter.toggleGroup(position);
   1267         } else {
   1268             Intent intent = new Intent(this, CallDetailActivity.class);
   1269             intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id));
   1270             StickyTabs.setTab(intent, getIntent());
   1271             startActivity(intent);
   1272         }
   1273     }
   1274 
   1275     @Override
   1276     public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
   1277             boolean globalSearch) {
   1278         if (globalSearch) {
   1279             super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
   1280         } else {
   1281             ContactsSearchManager.startSearch(this, initialQuery);
   1282         }
   1283     }
   1284 }
   1285