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