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