• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onClick(View view)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 
onPreDraw()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 
RecentCallsAdapter()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
onContentChanged()271         protected void onContentChanged() {
272             // Start async requery
273             startQuery();
274         }
275 
setLoading(boolean loading)276         void setLoading(boolean loading) {
277             mLoading = loading;
278         }
279 
280         @Override
isEmpty()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 
getContactInfo(String number)290         public ContactInfo getContactInfo(String number) {
291             return mContactInfo.get(number);
292         }
293 
startRequestProcessing()294         public void startRequestProcessing() {
295             mDone = false;
296             mCallerIdThread = new Thread(this);
297             mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
298             mCallerIdThread.start();
299         }
300 
stopRequestProcessing()301         public void stopRequestProcessing() {
302             mDone = true;
303             if (mCallerIdThread != null) mCallerIdThread.interrupt();
304         }
305 
clearCache()306         public void clearCache() {
307             synchronized (mContactInfo) {
308                 mContactInfo.clear();
309             }
310         }
311 
updateCallLog(CallerInfoQuery ciq, ContactInfo ci)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 
enqueueRequest(String number, int position, String name, int numberType, String numberLabel)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 
queryContactInfo(CallerInfoQuery ciq)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          */
run()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
addGroups(Cursor cursor)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 
equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2)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
newStandAloneView(Context context, ViewGroup parent)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
bindStandAloneView(View view, Context context, Cursor cursor)563         protected void bindStandAloneView(View view, Context context, Cursor cursor) {
564             bindView(context, view, cursor);
565         }
566 
567         @Override
newChildView(Context context, ViewGroup parent)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
bindChildView(View view, Context context, Cursor cursor)577         protected void bindChildView(View view, Context context, Cursor cursor) {
578             bindView(context, view, cursor);
579         }
580 
581         @Override
newGroupView(Context context, ViewGroup parent)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
bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)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 
findAndCacheViews(View view)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 
bindView(Context context, View view, Cursor c)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 {
CatchingWorkerHandler(Looper looper)785             public CatchingWorkerHandler(Looper looper) {
786                 super(looper);
787             }
788 
789             @Override
handleMessage(Message msg)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
createHandler(Looper looper)805         protected Handler createHandler(Looper looper) {
806             // Provide our special handler that catches exceptions
807             return new CatchingWorkerHandler(looper);
808         }
809 
QueryHandler(Context context)810         public QueryHandler(Context context) {
811             super(context.getContentResolver());
812             mActivity = new WeakReference<RecentCallsListActivity>(
813                     (RecentCallsListActivity) context);
814         }
815 
816         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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
onCreate(Bundle state)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
onStart()858     protected void onStart() {
859         mScrollToTop = true;
860         super.onStart();
861     }
862 
863     @Override
onResume()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
onPause()880     protected void onPause() {
881         super.onPause();
882 
883         // Kill the requests thread
884         mAdapter.stopRequestProcessing();
885     }
886 
887     @Override
onDestroy()888     protected void onDestroy() {
889         super.onDestroy();
890         mAdapter.stopRequestProcessing();
891         mAdapter.changeCursor(null);
892     }
893 
894     @Override
onWindowFocusChanged(boolean hasFocus)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      */
formatPhoneNumber(String number)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 
resetNewCallsFlag()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 
startQuery()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
onCreateOptionsMenu(Menu menu)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
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn)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
onCreateDialog(int id, Bundle args)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