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