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