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