CallLogFragment.java revision 8c559a9eaf12318b53de3611cd0e7e7ee0dc5a4e
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.calllog; 18 19import com.android.common.widget.GroupingListAdapter; 20import com.android.contacts.CallDetailActivity; 21import com.android.contacts.ContactPhotoManager; 22import com.android.contacts.ContactsUtils; 23import com.android.contacts.PhoneCallDetails; 24import com.android.contacts.PhoneCallDetailsHelper; 25import com.android.contacts.R; 26import com.android.contacts.activities.DialtactsActivity; 27import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener; 28import com.android.contacts.util.ExpirableCache; 29import com.android.internal.telephony.CallerInfo; 30import com.google.common.annotations.VisibleForTesting; 31 32import android.app.ListFragment; 33import android.content.AsyncQueryHandler; 34import android.content.ContentUris; 35import android.content.ContentValues; 36import android.content.Context; 37import android.content.Intent; 38import android.database.CharArrayBuffer; 39import android.database.Cursor; 40import android.database.sqlite.SQLiteDatabaseCorruptException; 41import android.database.sqlite.SQLiteDiskIOException; 42import android.database.sqlite.SQLiteException; 43import android.database.sqlite.SQLiteFullException; 44import android.graphics.drawable.Drawable; 45import android.net.Uri; 46import android.os.Bundle; 47import android.os.Handler; 48import android.os.Looper; 49import android.os.Message; 50import android.provider.CallLog.Calls; 51import android.provider.ContactsContract.CommonDataKinds.SipAddress; 52import android.provider.ContactsContract.Contacts; 53import android.provider.ContactsContract.Data; 54import android.provider.ContactsContract.Intents.Insert; 55import android.provider.ContactsContract.PhoneLookup; 56import android.telephony.PhoneNumberUtils; 57import android.telephony.TelephonyManager; 58import android.text.TextUtils; 59import android.util.Log; 60import android.view.ContextMenu; 61import android.view.ContextMenu.ContextMenuInfo; 62import android.view.LayoutInflater; 63import android.view.Menu; 64import android.view.MenuInflater; 65import android.view.MenuItem; 66import android.view.View; 67import android.view.ViewConfiguration; 68import android.view.ViewGroup; 69import android.view.ViewTreeObserver; 70import android.widget.AdapterView; 71import android.widget.ListView; 72import android.widget.QuickContactBadge; 73 74import java.lang.ref.WeakReference; 75import java.util.LinkedList; 76 77/** 78 * Displays a list of call log entries. 79 */ 80public class CallLogFragment extends ListFragment 81 implements View.OnCreateContextMenuListener, ViewPagerVisibilityListener { 82 private static final String TAG = "CallLogFragment"; 83 84 /** 85 * The size of the cache of contact info. 86 */ 87 private static final int CONTACT_INFO_CACHE_SIZE = 100; 88 89 /** The query for the call log table */ 90 private static final class CallLogQuery { 91 public static final String[] _PROJECTION = new String[] { 92 Calls._ID, 93 Calls.NUMBER, 94 Calls.DATE, 95 Calls.DURATION, 96 Calls.TYPE, 97 Calls.COUNTRY_ISO}; 98 99 public static final int ID = 0; 100 public static final int NUMBER = 1; 101 public static final int DATE = 2; 102 public static final int DURATION = 3; 103 public static final int CALL_TYPE = 4; 104 public static final int COUNTRY_ISO = 5; 105 } 106 107 /** The query to use for the phones table */ 108 private static final class PhoneQuery { 109 public static final String[] _PROJECTION = new String[] { 110 PhoneLookup._ID, 111 PhoneLookup.DISPLAY_NAME, 112 PhoneLookup.TYPE, 113 PhoneLookup.LABEL, 114 PhoneLookup.NUMBER, 115 PhoneLookup.NORMALIZED_NUMBER, 116 PhoneLookup.PHOTO_ID, 117 PhoneLookup.LOOKUP_KEY}; 118 119 public static final int PERSON_ID = 0; 120 public static final int NAME = 1; 121 public static final int PHONE_TYPE = 2; 122 public static final int LABEL = 3; 123 public static final int MATCHED_NUMBER = 4; 124 public static final int NORMALIZED_NUMBER = 5; 125 public static final int PHOTO_ID = 6; 126 public static final int LOOKUP_KEY = 7; 127 } 128 129 private static final class MenuItems { 130 public static final int DELETE = 1; 131 } 132 133 private static final class OptionsMenuItems { 134 public static final int DELETE_ALL = 1; 135 } 136 137 private static final int QUERY_TOKEN = 53; 138 private static final int UPDATE_TOKEN = 54; 139 140 private CallLogAdapter mAdapter; 141 private QueryHandler mQueryHandler; 142 private String mVoiceMailNumber; 143 private String mCurrentCountryIso; 144 private boolean mScrollToTop; 145 146 private boolean mShowOptionsMenu; 147 148 public static final class ContactInfo { 149 public long personId; 150 public String name; 151 public int type; 152 public String label; 153 public String number; 154 public String formattedNumber; 155 public String normalizedNumber; 156 public long photoId; 157 public String lookupKey; 158 159 public static ContactInfo EMPTY = new ContactInfo(); 160 } 161 162 public static final class CallerInfoQuery { 163 public String number; 164 public int position; 165 public String name; 166 public int numberType; 167 public String numberLabel; 168 public long photoId; 169 public String lookupKey; 170 } 171 172 /** Adapter class to fill in data for the Call Log */ 173 public final class CallLogAdapter extends GroupingListAdapter 174 implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener { 175 /** The time in millis to delay starting the thread processing requests. */ 176 private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; 177 178 /** 179 * A cache of the contact details for the phone numbers in the call log. 180 * <p> 181 * The content of the cache is expired (but not purged) whenever the application comes to 182 * the foreground. 183 */ 184 private ExpirableCache<String, ContactInfo> mContactInfoCache; 185 186 /** 187 * List of requests to update contact details. 188 * <p> 189 * The requests are added when displaying the contacts and are processed by a background 190 * thread. 191 */ 192 private final LinkedList<CallerInfoQuery> mRequests; 193 194 private volatile boolean mDone; 195 private boolean mLoading = true; 196 private ViewTreeObserver.OnPreDrawListener mPreDrawListener; 197 private static final int REDRAW = 1; 198 private static final int START_THREAD = 2; 199 private boolean mFirst; 200 private Thread mCallerIdThread; 201 202 /** Instance of helper class for managing views. */ 203 private final CallLogListItemHelper mCallLogViewsHelper; 204 205 /** 206 * Reusable char array buffers. 207 */ 208 private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128); 209 private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128); 210 /** Helper to set up contact photos. */ 211 private final ContactPhotoManager mContactPhotoManager; 212 213 /** Can be set to true by tests to disable processing of requests. */ 214 private volatile boolean mRequestProcessingDisabled = false; 215 216 @Override 217 public void onClick(View view) { 218 String number = (String) view.getTag(); 219 if (!TextUtils.isEmpty(number)) { 220 // Here, "number" can either be a PSTN phone number or a 221 // SIP address. So turn it into either a tel: URI or a 222 // sip: URI, as appropriate. 223 Uri callUri; 224 if (PhoneNumberUtils.isUriNumber(number)) { 225 callUri = Uri.fromParts("sip", number, null); 226 } else { 227 callUri = Uri.fromParts("tel", number, null); 228 } 229 startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri)); 230 } 231 } 232 233 @Override 234 public boolean onPreDraw() { 235 if (mFirst) { 236 mHandler.sendEmptyMessageDelayed(START_THREAD, 237 START_PROCESSING_REQUESTS_DELAY_MILLIS); 238 mFirst = false; 239 } 240 return true; 241 } 242 243 private Handler mHandler = new Handler() { 244 @Override 245 public void handleMessage(Message msg) { 246 switch (msg.what) { 247 case REDRAW: 248 notifyDataSetChanged(); 249 break; 250 case START_THREAD: 251 startRequestProcessing(); 252 break; 253 } 254 } 255 }; 256 257 public CallLogAdapter() { 258 super(getActivity()); 259 260 mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 261 mRequests = new LinkedList<CallerInfoQuery>(); 262 mPreDrawListener = null; 263 264 Drawable incomingDrawable = getResources().getDrawable( 265 R.drawable.ic_call_log_list_incoming_call); 266 Drawable outgoingDrawable = getResources().getDrawable( 267 R.drawable.ic_call_log_list_outgoing_call); 268 Drawable missedDrawable = getResources().getDrawable( 269 R.drawable.ic_call_log_list_missed_call); 270 Drawable voicemailDrawable = getResources().getDrawable( 271 R.drawable.ic_call_log_list_voicemail); 272 Drawable callDrawable = getResources().getDrawable( 273 R.drawable.ic_call_log_list_action_call); 274 Drawable playDrawable = getResources().getDrawable( 275 R.drawable.ic_call_log_list_action_play); 276 277 mContactPhotoManager = ContactPhotoManager.getInstance(getActivity()); 278 PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( 279 getActivity(), getResources(), mVoiceMailNumber, incomingDrawable, 280 outgoingDrawable, missedDrawable, voicemailDrawable); 281 mCallLogViewsHelper = new CallLogListItemHelper(phoneCallDetailsHelper, callDrawable, 282 playDrawable); 283 } 284 285 /** 286 * Requery on background thread when {@link Cursor} changes. 287 */ 288 @Override 289 protected void onContentChanged() { 290 // Start async requery 291 startQuery(); 292 } 293 294 void setLoading(boolean loading) { 295 mLoading = loading; 296 } 297 298 @Override 299 public boolean isEmpty() { 300 if (mLoading) { 301 // We don't want the empty state to show when loading. 302 return false; 303 } else { 304 return super.isEmpty(); 305 } 306 } 307 308 public ContactInfo getContactInfo(String number) { 309 return mContactInfoCache.getPossiblyExpired(number); 310 } 311 312 public void startRequestProcessing() { 313 if (mRequestProcessingDisabled) { 314 return; 315 } 316 317 mDone = false; 318 mCallerIdThread = new Thread(this); 319 mCallerIdThread.setPriority(Thread.MIN_PRIORITY); 320 mCallerIdThread.start(); 321 } 322 323 /** 324 * Stops the background thread that processes updates and cancels any pending requests to 325 * start it. 326 * <p> 327 * Should be called from the main thread to prevent a race condition between the request to 328 * start the thread being processed and stopping the thread. 329 */ 330 public void stopRequestProcessing() { 331 // Remove any pending requests to start the processing thread. 332 mHandler.removeMessages(START_THREAD); 333 mDone = true; 334 if (mCallerIdThread != null) mCallerIdThread.interrupt(); 335 } 336 337 public void invalidateCache() { 338 mContactInfoCache.expireAll(); 339 } 340 341 private void enqueueRequest(String number, boolean immediate, int position, 342 String name, int numberType, String numberLabel, long photoId, String lookupKey) { 343 CallerInfoQuery ciq = new CallerInfoQuery(); 344 ciq.number = number; 345 ciq.position = position; 346 ciq.name = name; 347 ciq.numberType = numberType; 348 ciq.numberLabel = numberLabel; 349 ciq.photoId = photoId; 350 ciq.lookupKey = lookupKey; 351 synchronized (mRequests) { 352 mRequests.add(ciq); 353 mRequests.notifyAll(); 354 } 355 if (mFirst && immediate) { 356 startRequestProcessing(); 357 mFirst = false; 358 } 359 } 360 361 private boolean queryContactInfo(CallerInfoQuery ciq) { 362 // First check if there was a prior request for the same number 363 // that was already satisfied 364 ContactInfo info = mContactInfoCache.get(ciq.number); 365 boolean needNotify = false; 366 if (info != null && info != ContactInfo.EMPTY) { 367 return true; 368 } else { 369 // Ok, do a fresh Contacts lookup for ciq.number. 370 boolean infoUpdated = false; 371 372 if (PhoneNumberUtils.isUriNumber(ciq.number)) { 373 // This "number" is really a SIP address. 374 375 // TODO: This code is duplicated from the 376 // CallerInfoAsyncQuery class. To avoid that, could the 377 // code here just use CallerInfoAsyncQuery, rather than 378 // manually running ContentResolver.query() itself? 379 380 // We look up SIP addresses directly in the Data table: 381 Uri contactRef = Data.CONTENT_URI; 382 383 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. 384 // 385 // Also note we use "upper(data1)" in the WHERE clause, and 386 // uppercase the incoming SIP address, in order to do a 387 // case-insensitive match. 388 // 389 // TODO: May also need to normalize by adding "sip:" as a 390 // prefix, if we start storing SIP addresses that way in the 391 // database. 392 String selection = "upper(" + Data.DATA1 + ")=?" 393 + " AND " 394 + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'"; 395 String[] selectionArgs = new String[] { ciq.number.toUpperCase() }; 396 397 Cursor dataTableCursor = 398 getActivity().getContentResolver().query( 399 contactRef, 400 null, // projection 401 selection, // selection 402 selectionArgs, // selectionArgs 403 null); // sortOrder 404 405 if (dataTableCursor != null) { 406 if (dataTableCursor.moveToFirst()) { 407 info = new ContactInfo(); 408 409 // TODO: we could slightly speed this up using an 410 // explicit projection (and thus not have to do 411 // those getColumnIndex() calls) but the benefit is 412 // very minimal. 413 414 // Note the Data.CONTACT_ID column here is 415 // equivalent to the PERSON_ID_COLUMN_INDEX column 416 // we use with "phonesCursor" below. 417 info.personId = dataTableCursor.getLong( 418 dataTableCursor.getColumnIndex(Data.CONTACT_ID)); 419 info.name = dataTableCursor.getString( 420 dataTableCursor.getColumnIndex(Data.DISPLAY_NAME)); 421 // "type" and "label" are currently unused for SIP addresses 422 info.type = SipAddress.TYPE_OTHER; 423 info.label = null; 424 425 // And "number" is the SIP address. 426 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. 427 info.number = dataTableCursor.getString( 428 dataTableCursor.getColumnIndex(Data.DATA1)); 429 info.normalizedNumber = null; // meaningless for SIP addresses 430 info.photoId = dataTableCursor.getLong( 431 dataTableCursor.getColumnIndex(Data.PHOTO_ID)); 432 info.lookupKey = dataTableCursor.getString( 433 dataTableCursor.getColumnIndex(Data.LOOKUP_KEY)); 434 435 infoUpdated = true; 436 } 437 dataTableCursor.close(); 438 } 439 } else { 440 // "number" is a regular phone number, so use the 441 // PhoneLookup table: 442 Cursor phonesCursor = 443 getActivity().getContentResolver().query( 444 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 445 Uri.encode(ciq.number)), 446 PhoneQuery._PROJECTION, null, null, null); 447 if (phonesCursor != null) { 448 if (phonesCursor.moveToFirst()) { 449 info = new ContactInfo(); 450 info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID); 451 info.name = phonesCursor.getString(PhoneQuery.NAME); 452 info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE); 453 info.label = phonesCursor.getString(PhoneQuery.LABEL); 454 info.number = phonesCursor 455 .getString(PhoneQuery.MATCHED_NUMBER); 456 info.normalizedNumber = phonesCursor 457 .getString(PhoneQuery.NORMALIZED_NUMBER); 458 info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID); 459 info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY); 460 461 infoUpdated = true; 462 } 463 phonesCursor.close(); 464 } 465 } 466 467 if (infoUpdated) { 468 // New incoming phone number invalidates our formatted 469 // cache. Any cache fills happen only on the GUI thread. 470 info.formattedNumber = null; 471 472 mContactInfoCache.put(ciq.number, info); 473 474 // Inform list to update this item, if in view 475 needNotify = true; 476 } 477 } 478 return needNotify; 479 } 480 481 /* 482 * Handles requests for contact name and number type 483 * @see java.lang.Runnable#run() 484 */ 485 @Override 486 public void run() { 487 boolean needNotify = false; 488 while (!mDone) { 489 CallerInfoQuery ciq = null; 490 synchronized (mRequests) { 491 if (!mRequests.isEmpty()) { 492 ciq = mRequests.removeFirst(); 493 } else { 494 if (needNotify) { 495 needNotify = false; 496 mHandler.sendEmptyMessage(REDRAW); 497 } 498 try { 499 mRequests.wait(1000); 500 } catch (InterruptedException ie) { 501 // Ignore and continue processing requests 502 Thread.currentThread().interrupt(); 503 } 504 } 505 } 506 if (!mDone && ciq != null && queryContactInfo(ciq)) { 507 needNotify = true; 508 } 509 } 510 } 511 512 @Override 513 protected void addGroups(Cursor cursor) { 514 515 int count = cursor.getCount(); 516 if (count == 0) { 517 return; 518 } 519 520 int groupItemCount = 1; 521 522 CharArrayBuffer currentValue = mBuffer1; 523 CharArrayBuffer value = mBuffer2; 524 cursor.moveToFirst(); 525 cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue); 526 int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE); 527 for (int i = 1; i < count; i++) { 528 cursor.moveToNext(); 529 cursor.copyStringToBuffer(CallLogQuery.NUMBER, value); 530 boolean sameNumber = equalPhoneNumbers(value, currentValue); 531 532 // Group adjacent calls with the same number. Make an exception 533 // for the latest item if it was a missed call. We don't want 534 // a missed call to be hidden inside a group. 535 if (sameNumber && currentCallType != Calls.MISSED_TYPE) { 536 groupItemCount++; 537 } else { 538 if (groupItemCount > 1) { 539 addGroup(i - groupItemCount, groupItemCount, false); 540 } 541 542 groupItemCount = 1; 543 544 // Swap buffers 545 CharArrayBuffer temp = currentValue; 546 currentValue = value; 547 value = temp; 548 549 // If we have just examined a row following a missed call, make 550 // sure that it is grouped with subsequent calls from the same number 551 // even if it was also missed. 552 if (sameNumber && currentCallType == Calls.MISSED_TYPE) { 553 currentCallType = 0; // "not a missed call" 554 } else { 555 currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE); 556 } 557 } 558 } 559 if (groupItemCount > 1) { 560 addGroup(count - groupItemCount, groupItemCount, false); 561 } 562 } 563 564 protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { 565 566 // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid 567 // string allocation 568 return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied), 569 new String(buffer2.data, 0, buffer2.sizeCopied)); 570 } 571 572 573 @VisibleForTesting 574 @Override 575 public View newStandAloneView(Context context, ViewGroup parent) { 576 LayoutInflater inflater = 577 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 578 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 579 findAndCacheViews(view); 580 return view; 581 } 582 583 @VisibleForTesting 584 @Override 585 public void bindStandAloneView(View view, Context context, Cursor cursor) { 586 bindView(view, cursor, 1); 587 } 588 589 @VisibleForTesting 590 @Override 591 public View newChildView(Context context, ViewGroup parent) { 592 LayoutInflater inflater = 593 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 594 View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false); 595 findAndCacheViews(view); 596 return view; 597 } 598 599 @VisibleForTesting 600 @Override 601 public void bindChildView(View view, Context context, Cursor cursor) { 602 bindView(view, cursor, 1); 603 } 604 605 @VisibleForTesting 606 @Override 607 public View newGroupView(Context context, ViewGroup parent) { 608 LayoutInflater inflater = 609 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 610 View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false); 611 findAndCacheViews(view); 612 return view; 613 } 614 615 @VisibleForTesting 616 @Override 617 public void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 618 boolean expanded) { 619 bindView(view, cursor, groupSize); 620 } 621 622 private void findAndCacheViews(View view) { 623 // Get the views to bind to. 624 CallLogListItemViews views = CallLogListItemViews.fromView(view); 625 if (views.callView != null) { 626 views.callView.setOnClickListener(this); 627 } 628 view.setTag(views); 629 } 630 631 /** 632 * Binds the views in the entry to the data in the call log. 633 * 634 * @param view the view corresponding to this entry 635 * @param c the cursor pointing to the entry in the call log 636 * @param count the number of entries in the current item, greater than 1 if it is a group 637 */ 638 public void bindView(View view, Cursor c, int count) { 639 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 640 641 String number = c.getString(CallLogQuery.NUMBER); 642 long date = c.getLong(CallLogQuery.DATE); 643 int callType = c.getInt(CallLogQuery.CALL_TYPE); 644 final String formattedNumber; 645 String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 646 // Store away the number so we can call it directly if you click on the call icon 647 if (views.callView != null) { 648 views.callView.setTag(number); 649 } 650 651 // Lookup contacts with this number 652 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 653 mContactInfoCache.getCachedValue(number); 654 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 655 if (cachedInfo == null) { 656 // Mark it as empty and queue up a request to find the name 657 // The db request should happen on a non-UI thread 658 info = ContactInfo.EMPTY; 659 // Format the cached call_log phone number 660 formattedNumber = formatPhoneNumber(number, null, countryIso); 661 mContactInfoCache.put(number, info); 662 Log.d(TAG, "Contact info missing: " + number); 663 // Request the contact details immediately since they are currently missing. 664 enqueueRequest(number, true, c.getPosition(), "", 0, "", 0L, ""); 665 } else if (info != ContactInfo.EMPTY) { // Has been queried 666 if (cachedInfo.isExpired()) { 667 Log.d(TAG, "Contact info expired: " + number); 668 // Put it back in the cache, therefore marking it as not expired, so that other 669 // entries with the same number will not re-request it. 670 mContactInfoCache.put(number, info); 671 // The contact info is no longer up to date, we should request it. However, we 672 // do not need to request them immediately. 673 enqueueRequest(number, false, c.getPosition(), info.name, info.type, info.label, 674 info.photoId, info.lookupKey); 675 } 676 677 // Format and cache phone number for found contact 678 if (info.formattedNumber == null) { 679 info.formattedNumber = 680 formatPhoneNumber(info.number, info.normalizedNumber, countryIso); 681 } 682 formattedNumber = info.formattedNumber; 683 } else { 684 // Format the cached call_log phone number 685 formattedNumber = formatPhoneNumber(number, null, countryIso); 686 } 687 688 long contactId = info.personId; 689 String name = info.name; 690 int ntype = info.type; 691 String label = info.label; 692 long photoId = info.photoId; 693 String lookupKey = info.lookupKey; 694 // Assumes the call back feature is on most of the 695 // time. For private and unknown numbers: hide it. 696 if (views.callView != null) { 697 views.callView.setVisibility(View.VISIBLE); 698 } 699 700 final PhoneCallDetails details; 701 if (TextUtils.isEmpty(name)) { 702 details = new PhoneCallDetails(number, formattedNumber, callType, date); 703 } else { 704 details = new PhoneCallDetails(number, formattedNumber, callType, date, name, 705 ntype, label); 706 } 707 mCallLogViewsHelper.setPhoneCallDetails(views, details , true); 708 if (views.photoView != null) { 709 bindQuickContact(views.photoView, photoId, contactId, lookupKey); 710 } 711 712 713 // Listen for the first draw 714 if (mPreDrawListener == null) { 715 mFirst = true; 716 mPreDrawListener = this; 717 view.getViewTreeObserver().addOnPreDrawListener(this); 718 } 719 } 720 721 private void bindQuickContact(QuickContactBadge view, long photoId, long contactId, 722 String lookupKey) { 723 view.assignContactUri(getContactUri(contactId, lookupKey)); 724 mContactPhotoManager.loadPhoto(view, photoId); 725 } 726 727 private Uri getContactUri(long contactId, String lookupKey) { 728 return Contacts.getLookupUri(contactId, lookupKey); 729 } 730 731 /** 732 * Sets whether processing of requests for contact details should be enabled. 733 * <p> 734 * This method should be called in tests to disable such processing of requests when not 735 * needed. 736 */ 737 public void disableRequestProcessingForTest() { 738 mRequestProcessingDisabled = true; 739 } 740 741 public void injectContactInfoForTest(String number, ContactInfo contactInfo) { 742 mContactInfoCache.put(number, contactInfo); 743 } 744 } 745 746 private static final class QueryHandler extends AsyncQueryHandler { 747 private final WeakReference<CallLogFragment> mFragment; 748 749 /** 750 * Simple handler that wraps background calls to catch 751 * {@link SQLiteException}, such as when the disk is full. 752 */ 753 protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { 754 public CatchingWorkerHandler(Looper looper) { 755 super(looper); 756 } 757 758 @Override 759 public void handleMessage(Message msg) { 760 try { 761 // Perform same query while catching any exceptions 762 super.handleMessage(msg); 763 } catch (SQLiteDiskIOException e) { 764 Log.w(TAG, "Exception on background worker thread", e); 765 } catch (SQLiteFullException e) { 766 Log.w(TAG, "Exception on background worker thread", e); 767 } catch (SQLiteDatabaseCorruptException e) { 768 Log.w(TAG, "Exception on background worker thread", e); 769 } 770 } 771 } 772 773 @Override 774 protected Handler createHandler(Looper looper) { 775 // Provide our special handler that catches exceptions 776 return new CatchingWorkerHandler(looper); 777 } 778 779 public QueryHandler(CallLogFragment fragment) { 780 super(fragment.getActivity().getContentResolver()); 781 mFragment = new WeakReference<CallLogFragment>(fragment); 782 } 783 784 @Override 785 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 786 final CallLogFragment fragment = mFragment.get(); 787 if (fragment != null && fragment.getActivity() != null && 788 !fragment.getActivity().isFinishing()) { 789 final CallLogFragment.CallLogAdapter callsAdapter = fragment.mAdapter; 790 callsAdapter.setLoading(false); 791 callsAdapter.changeCursor(cursor); 792 if (fragment.mScrollToTop) { 793 final ListView listView = fragment.getListView(); 794 if (listView.getFirstVisiblePosition() > 5) { 795 listView.setSelection(5); 796 } 797 listView.smoothScrollToPosition(0); 798 fragment.mScrollToTop = false; 799 } 800 } else { 801 cursor.close(); 802 } 803 } 804 } 805 806 @Override 807 public void onCreate(Bundle state) { 808 super.onCreate(state); 809 810 mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService( 811 Context.TELEPHONY_SERVICE)).getVoiceMailNumber(); 812 mQueryHandler = new QueryHandler(this); 813 814 mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); 815 816 setHasOptionsMenu(true); 817 } 818 819 @Override 820 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 821 return inflater.inflate(R.layout.call_log_fragment, container, false); 822 } 823 824 @Override 825 public void onViewCreated(View view, Bundle savedInstanceState) { 826 super.onViewCreated(view, savedInstanceState); 827 getListView().setOnCreateContextMenuListener(this); 828 mAdapter = new CallLogAdapter(); 829 setListAdapter(mAdapter); 830 } 831 832 @Override 833 public void onStart() { 834 mScrollToTop = true; 835 super.onStart(); 836 } 837 838 @Override 839 public void onResume() { 840 // Mark all entries in the contact info cache as out of date, so they will be looked up 841 // again once being shown. 842 if (mAdapter != null) { 843 mAdapter.invalidateCache(); 844 } 845 846 startQuery(); 847 resetNewCallsFlag(); 848 849 super.onResume(); 850 851 mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw 852 } 853 854 @Override 855 public void onPause() { 856 super.onPause(); 857 858 // Kill the requests thread 859 mAdapter.stopRequestProcessing(); 860 } 861 862 @Override 863 public void onDestroy() { 864 super.onDestroy(); 865 mAdapter.stopRequestProcessing(); 866 mAdapter.changeCursor(null); 867 } 868 869 /** 870 * Format the given phone number 871 * 872 * @param number the number to be formatted. 873 * @param normalizedNumber the normalized number of the given number. 874 * @param countryIso the ISO 3166-1 two letters country code, the country's 875 * convention will be used to format the number if the normalized 876 * phone is null. 877 * 878 * @return the formatted number, or the given number if it was formatted. 879 */ 880 private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { 881 if (TextUtils.isEmpty(number)) { 882 return ""; 883 } 884 // If "number" is really a SIP address, don't try to do any formatting at all. 885 if (PhoneNumberUtils.isUriNumber(number)) { 886 return number; 887 } 888 if (TextUtils.isEmpty(countryIso)) { 889 countryIso = mCurrentCountryIso; 890 } 891 return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); 892 } 893 894 private void resetNewCallsFlag() { 895 // Mark all "new" missed calls as not new anymore 896 StringBuilder where = new StringBuilder("type="); 897 where.append(Calls.MISSED_TYPE); 898 where.append(" AND new=1"); 899 900 ContentValues values = new ContentValues(1); 901 values.put(Calls.NEW, "0"); 902 mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 903 values, where.toString(), null); 904 } 905 906 private void startQuery() { 907 mAdapter.setLoading(true); 908 909 // Cancel any pending queries 910 mQueryHandler.cancelOperation(QUERY_TOKEN); 911 mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL, 912 CallLogQuery._PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER); 913 } 914 915 @Override 916 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 917 super.onCreateOptionsMenu(menu, inflater); 918 inflater.inflate(R.menu.call_log_options, menu); 919 } 920 921 @Override 922 public void onPrepareOptionsMenu(Menu menu) { 923 menu.findItem(R.id.delete_all).setVisible(mShowOptionsMenu); 924 final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_call_log); 925 if (mShowOptionsMenu) { 926 callSettingsMenuItem.setVisible(true); 927 callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent()); 928 } else { 929 callSettingsMenuItem.setVisible(false); 930 } 931 } 932 933 @Override 934 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 935 AdapterView.AdapterContextMenuInfo menuInfo; 936 try { 937 menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn; 938 } catch (ClassCastException e) { 939 Log.e(TAG, "bad menuInfoIn", e); 940 return; 941 } 942 943 Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position); 944 945 String number = cursor.getString(CallLogQuery.NUMBER); 946 Uri numberUri = null; 947 boolean isVoicemail = false; 948 boolean isSipNumber = false; 949 if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { 950 number = getString(R.string.unknown); 951 } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { 952 number = getString(R.string.private_num); 953 } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { 954 number = getString(R.string.payphone); 955 } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) { 956 number = getString(R.string.voicemail); 957 numberUri = Uri.parse("voicemail:x"); 958 isVoicemail = true; 959 } else if (PhoneNumberUtils.isUriNumber(number)) { 960 numberUri = Uri.fromParts("sip", number, null); 961 isSipNumber = true; 962 } else { 963 numberUri = Uri.fromParts("tel", number, null); 964 } 965 966 ContactInfo info = mAdapter.getContactInfo(number); 967 boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY); 968 if (contactInfoPresent) { 969 menu.setHeaderTitle(info.name); 970 } else { 971 menu.setHeaderTitle(number); 972 } 973 974 if (numberUri != null) { 975 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri); 976 menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number)) 977 .setIntent(intent); 978 } 979 980 if (contactInfoPresent) { 981 menu.add(0, 0, 0, R.string.menu_viewContact) 982 .setIntent(new Intent(Intent.ACTION_VIEW, 983 ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId))); 984 } 985 986 if (numberUri != null && !isVoicemail && !isSipNumber) { 987 menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall) 988 .setIntent(new Intent(Intent.ACTION_DIAL, numberUri)); 989 menu.add(0, 0, 0, R.string.menu_sendTextMessage) 990 .setIntent(new Intent(Intent.ACTION_SENDTO, 991 Uri.fromParts("sms", number, null))); 992 } 993 994 // "Add to contacts" item, if this entry isn't already associated with a contact 995 if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) { 996 // TODO: This item is currently disabled for SIP addresses, because 997 // the Insert.PHONE extra only works correctly for PSTN numbers. 998 // 999 // To fix this for SIP addresses, we need to: 1000 // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if 1001 // the current number is a SIP address 1002 // - update the contacts UI code to handle Insert.SIP_ADDRESS by 1003 // updating the SipAddress field 1004 // and then we can remove the "!isSipNumber" check above. 1005 1006 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1007 intent.setType(Contacts.CONTENT_ITEM_TYPE); 1008 intent.putExtra(Insert.PHONE, number); 1009 menu.add(0, 0, 0, R.string.recentCalls_addToContact) 1010 .setIntent(intent); 1011 } 1012 menu.add(0, MenuItems.DELETE, 0, R.string.recentCalls_removeFromRecentList); 1013 } 1014 1015 @Override 1016 public boolean onOptionsItemSelected(MenuItem item) { 1017 switch (item.getItemId()) { 1018 case OptionsMenuItems.DELETE_ALL: { 1019 ClearCallLogDialog.show(getFragmentManager()); 1020 return true; 1021 } 1022 } 1023 return super.onOptionsItemSelected(item); 1024 } 1025 1026 @Override 1027 public boolean onContextItemSelected(MenuItem item) { 1028 // Convert the menu info to the proper type 1029 AdapterView.AdapterContextMenuInfo menuInfo; 1030 try { 1031 menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1032 } catch (ClassCastException e) { 1033 Log.e(TAG, "bad menuInfoIn", e); 1034 return false; 1035 } 1036 1037 switch (item.getItemId()) { 1038 case MenuItems.DELETE: { 1039 Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position); 1040 int groupSize = 1; 1041 if (mAdapter.isGroupHeader(menuInfo.position)) { 1042 groupSize = mAdapter.getGroupSize(menuInfo.position); 1043 } 1044 1045 StringBuilder sb = new StringBuilder(); 1046 for (int i = 0; i < groupSize; i++) { 1047 if (i != 0) { 1048 sb.append(","); 1049 cursor.moveToNext(); 1050 } 1051 long id = cursor.getLong(CallLogQuery.ID); 1052 sb.append(id); 1053 } 1054 1055 getActivity().getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL, 1056 Calls._ID + " IN (" + sb + ")", null); 1057 } 1058 } 1059 return super.onContextItemSelected(item); 1060 } 1061 1062 /* 1063 * Get the number from the Contacts, if available, since sometimes 1064 * the number provided by caller id may not be formatted properly 1065 * depending on the carrier (roaming) in use at the time of the 1066 * incoming call. 1067 * Logic : If the caller-id number starts with a "+", use it 1068 * Else if the number in the contacts starts with a "+", use that one 1069 * Else if the number in the contacts is longer, use that one 1070 */ 1071 private String getBetterNumberFromContacts(String number) { 1072 String matchingNumber = null; 1073 // Look in the cache first. If it's not found then query the Phones db 1074 ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number); 1075 if (ci != null && ci != ContactInfo.EMPTY) { 1076 matchingNumber = ci.number; 1077 } else { 1078 try { 1079 Cursor phonesCursor = getActivity().getContentResolver().query( 1080 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 1081 PhoneQuery._PROJECTION, null, null, null); 1082 if (phonesCursor != null) { 1083 if (phonesCursor.moveToFirst()) { 1084 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 1085 } 1086 phonesCursor.close(); 1087 } 1088 } catch (Exception e) { 1089 // Use the number from the call log 1090 } 1091 } 1092 if (!TextUtils.isEmpty(matchingNumber) && 1093 (matchingNumber.startsWith("+") 1094 || matchingNumber.length() > number.length())) { 1095 number = matchingNumber; 1096 } 1097 return number; 1098 } 1099 1100 public void callSelectedEntry() { 1101 int position = getListView().getSelectedItemPosition(); 1102 if (position < 0) { 1103 // In touch mode you may often not have something selected, so 1104 // just call the first entry to make sure that [send] [send] calls the 1105 // most recent entry. 1106 position = 0; 1107 } 1108 final Cursor cursor = (Cursor)mAdapter.getItem(position); 1109 if (cursor != null) { 1110 String number = cursor.getString(CallLogQuery.NUMBER); 1111 if (TextUtils.isEmpty(number) 1112 || number.equals(CallerInfo.UNKNOWN_NUMBER) 1113 || number.equals(CallerInfo.PRIVATE_NUMBER) 1114 || number.equals(CallerInfo.PAYPHONE_NUMBER)) { 1115 // This number can't be called, do nothing 1116 return; 1117 } 1118 Intent intent; 1119 // If "number" is really a SIP address, construct a sip: URI. 1120 if (PhoneNumberUtils.isUriNumber(number)) { 1121 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1122 Uri.fromParts("sip", number, null)); 1123 } else { 1124 // We're calling a regular PSTN phone number. 1125 // Construct a tel: URI, but do some other possible cleanup first. 1126 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 1127 if (!number.startsWith("+") && 1128 (callType == Calls.INCOMING_TYPE 1129 || callType == Calls.MISSED_TYPE)) { 1130 // If the caller-id matches a contact with a better qualified number, use it 1131 number = getBetterNumberFromContacts(number); 1132 } 1133 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1134 Uri.fromParts("tel", number, null)); 1135 } 1136 intent.setFlags( 1137 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 1138 startActivity(intent); 1139 } 1140 } 1141 1142 @Override 1143 public void onListItemClick(ListView l, View v, int position, long id) { 1144 if (mAdapter.isGroupHeader(position)) { 1145 mAdapter.toggleGroup(position); 1146 } else { 1147 Intent intent = new Intent(getActivity(), CallDetailActivity.class); 1148 intent.setData(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, id)); 1149 startActivity(intent); 1150 } 1151 } 1152 1153 @VisibleForTesting 1154 public CallLogAdapter getAdapter() { 1155 return mAdapter; 1156 } 1157 1158 @VisibleForTesting 1159 public String getVoiceMailNumber() { 1160 return mVoiceMailNumber; 1161 } 1162 1163 @Override 1164 public void onVisibilityChanged(boolean visible) { 1165 mShowOptionsMenu = visible; 1166 } 1167} 1168