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