CallLogFragment.java revision ae221f1da4d6d86c3620f1217038be442ff37edb
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.TextView; 67 68import java.util.LinkedList; 69import java.util.List; 70 71/** 72 * Displays a list of call log entries. 73 */ 74public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener, 75 CallLogQueryHandler.Listener { 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 = -1; 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 // Do nothing when a plain photo is clicked. Without this, the list item will fire. 660 views.plainPhotoView.setOnClickListener(null); 661 view.setTag(views); 662 } 663 664 /** 665 * Binds the views in the entry to the data in the call log. 666 * 667 * @param view the view corresponding to this entry 668 * @param c the cursor pointing to the entry in the call log 669 * @param count the number of entries in the current item, greater than 1 if it is a group 670 */ 671 private void bindView(View view, Cursor c, int count) { 672 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 673 final int section = c.getInt(CallLogQuery.SECTION); 674 675 // This might be a header: check the value of the section column in the cursor. 676 if (section == CallLogQuery.SECTION_NEW_HEADER 677 || section == CallLogQuery.SECTION_OLD_HEADER) { 678 views.listItemView.setVisibility(View.GONE); 679 views.listHeaderView.setVisibility(View.VISIBLE); 680 views.listHeaderTextView.setText( 681 section == CallLogQuery.SECTION_NEW_HEADER 682 ? R.string.call_log_new_header 683 : R.string.call_log_old_header); 684 // Nothing else to set up for a header. 685 return; 686 } 687 // Default case: an item in the call log. 688 views.listItemView.setVisibility(View.VISIBLE); 689 views.listHeaderView.setVisibility(View.GONE); 690 691 final String number = c.getString(CallLogQuery.NUMBER); 692 final long date = c.getLong(CallLogQuery.DATE); 693 final long duration = c.getLong(CallLogQuery.DURATION); 694 final int callType = c.getInt(CallLogQuery.CALL_TYPE); 695 final String formattedNumber; 696 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 697 698 // Store away the voicemail information so we can play it directly. 699 if (callType == Calls.VOICEMAIL_TYPE) { 700 String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 701 final long rowId = c.getLong(CallLogQuery.ID); 702 views.callView.setTag( 703 IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri)); 704 } else if (!TextUtils.isEmpty(number)) { 705 // Store away the number so we can call it directly if you click on the call icon. 706 views.callView.setTag(IntentProvider.getReturnCallIntentProvider(number)); 707 } else { 708 // No action enabled. 709 views.callView.setTag(null); 710 } 711 712 // Lookup contacts with this number 713 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 714 mContactInfoCache.getCachedValue(number); 715 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 716 if (cachedInfo == null) { 717 // Mark it as empty and queue up a request to find the name. 718 // The db request should happen on a non-UI thread. 719 info = ContactInfo.EMPTY; 720 mContactInfoCache.put(number, info); 721 // Request the contact details immediately since they are currently missing. 722 enqueueRequest(number, true); 723 // Format the phone number in the call log as best as we can. 724 formattedNumber = formatPhoneNumber(number, null, countryIso); 725 } else { 726 if (cachedInfo.isExpired()) { 727 // The contact info is no longer up to date, we should request it. However, we 728 // do not need to request them immediately. 729 enqueueRequest(number, false); 730 } 731 732 if (info != ContactInfo.EMPTY) { 733 // Format and cache phone number for found contact. 734 if (info.formattedNumber == null) { 735 info.formattedNumber = 736 formatPhoneNumber(info.number, info.normalizedNumber, countryIso); 737 } 738 formattedNumber = info.formattedNumber; 739 } else { 740 // Format the phone number in the call log as best as we can. 741 formattedNumber = formatPhoneNumber(number, null, countryIso); 742 } 743 } 744 745 final long personId = info.personId; 746 final String name = info.name; 747 final int ntype = info.type; 748 final String label = info.label; 749 final Uri thumbnailUri = info.thumbnailUri; 750 final String lookupKey = info.lookupKey; 751 final int[] callTypes = getCallTypes(c, count); 752 final PhoneCallDetails details; 753 if (TextUtils.isEmpty(name)) { 754 details = new PhoneCallDetails(number, formattedNumber, countryIso, 755 callTypes, date, duration); 756 } else { 757 details = new PhoneCallDetails(number, formattedNumber, countryIso, 758 callTypes, date, duration, name, ntype, label, personId, thumbnailUri); 759 } 760 761 final boolean isNew = CallLogQuery.isNewSection(c); 762 // Use icons for old items, but text for new ones. 763 final boolean useIcons = !isNew; 764 // New items also use the highlighted version of the text. 765 final boolean isHighlighted = isNew; 766 mCallLogViewsHelper.setPhoneCallDetails(views, details, useIcons, isHighlighted); 767 setPhoto(views, thumbnailUri, personId, lookupKey); 768 769 // Listen for the first draw 770 if (mPreDrawListener == null) { 771 mFirst = true; 772 mPreDrawListener = this; 773 view.getViewTreeObserver().addOnPreDrawListener(this); 774 } 775 } 776 777 /** 778 * Returns the call types for the given number of items in the cursor. 779 * <p> 780 * It uses the next {@code count} rows in the cursor to extract the types. 781 * <p> 782 * It position in the cursor is unchanged by this function. 783 */ 784 private int[] getCallTypes(Cursor cursor, int count) { 785 int position = cursor.getPosition(); 786 int[] callTypes = new int[count]; 787 for (int index = 0; index < count; ++index) { 788 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 789 cursor.moveToNext(); 790 } 791 cursor.moveToPosition(position); 792 return callTypes; 793 } 794 795 private void setPhoto(CallLogListItemViews views, Uri thumbnailUri, long contactId, 796 String lookupKey) { 797 if (contactId == -1) { 798 // This does not correspond to a contact, do not use the QuickContactBadge. 799 mContactPhotoManager.loadPhoto(views.plainPhotoView, thumbnailUri); 800 views.plainPhotoView.setVisibility(View.VISIBLE); 801 views.quickContactView.setVisibility(View.GONE); 802 } else { 803 views.quickContactView.assignContactUri( 804 Contacts.getLookupUri(contactId, lookupKey)); 805 mContactPhotoManager.loadPhoto(views.quickContactView, thumbnailUri); 806 views.quickContactView.setVisibility(View.VISIBLE); 807 views.plainPhotoView.setVisibility(View.GONE); 808 } 809 } 810 811 /** 812 * Sets whether processing of requests for contact details should be enabled. 813 * <p> 814 * This method should be called in tests to disable such processing of requests when not 815 * needed. 816 */ 817 public void disableRequestProcessingForTest() { 818 mRequestProcessingDisabled = true; 819 } 820 821 public void injectContactInfoForTest(String number, ContactInfo contactInfo) { 822 mContactInfoCache.put(number, contactInfo); 823 } 824 825 @Override 826 public void addGroup(int cursorPosition, int size, boolean expanded) { 827 super.addGroup(cursorPosition, size, expanded); 828 } 829 } 830 831 @Override 832 public void onCreate(Bundle state) { 833 super.onCreate(state); 834 835 mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService( 836 Context.TELEPHONY_SERVICE)).getVoiceMailNumber(); 837 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); 838 839 mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); 840 841 setHasOptionsMenu(true); 842 } 843 844 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 845 @Override 846 public void onCallsFetched(Cursor cursor) { 847 if (getActivity() == null || getActivity().isFinishing()) { 848 return; 849 } 850 mAdapter.setLoading(false); 851 mAdapter.changeCursor(cursor); 852 if (mScrollToTop) { 853 final ListView listView = getListView(); 854 if (listView.getFirstVisiblePosition() > 5) { 855 listView.setSelection(5); 856 } 857 listView.smoothScrollToPosition(0); 858 mScrollToTop = false; 859 } 860 } 861 862 /** 863 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 864 */ 865 @Override 866 public void onVoicemailStatusFetched(Cursor statusCursor) { 867 if (getActivity() == null || getActivity().isFinishing()) { 868 return; 869 } 870 updateVoicemailStatusMessage(statusCursor); 871 } 872 873 @Override 874 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 875 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 876 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 877 mStatusMessageView = view.findViewById(R.id.voicemail_status); 878 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 879 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 880 return view; 881 } 882 883 @Override 884 public void onViewCreated(View view, Bundle savedInstanceState) { 885 super.onViewCreated(view, savedInstanceState); 886 mAdapter = new CallLogAdapter(); 887 setListAdapter(mAdapter); 888 } 889 890 @Override 891 public void onStart() { 892 mScrollToTop = true; 893 super.onStart(); 894 } 895 896 @Override 897 public void onResume() { 898 super.onResume(); 899 refreshData(); 900 } 901 902 private void updateVoicemailStatusMessage(Cursor statusCursor) { 903 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 904 if (messages.size() == 0) { 905 mStatusMessageView.setVisibility(View.GONE); 906 } else { 907 mStatusMessageView.setVisibility(View.VISIBLE); 908 // TODO: Change the code to show all messages. For now just pick the first message. 909 final StatusMessage message = messages.get(0); 910 if (message.showInCallLog()) { 911 mStatusMessageText.setText(message.callLogMessageId); 912 } 913 if (message.actionMessageId != -1) { 914 mStatusMessageAction.setText(message.actionMessageId); 915 } 916 if (message.actionUri != null) { 917 mStatusMessageAction.setClickable(true); 918 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 919 @Override 920 public void onClick(View v) { 921 getActivity().startActivity( 922 new Intent(Intent.ACTION_VIEW, message.actionUri)); 923 } 924 }); 925 } else { 926 mStatusMessageAction.setClickable(false); 927 } 928 } 929 } 930 931 @Override 932 public void onPause() { 933 super.onPause(); 934 935 // Kill the requests thread 936 mAdapter.stopRequestProcessing(); 937 } 938 939 @Override 940 public void onDestroy() { 941 super.onDestroy(); 942 mAdapter.stopRequestProcessing(); 943 mAdapter.changeCursor(null); 944 } 945 946 /** 947 * Format the given phone number 948 * 949 * @param number the number to be formatted. 950 * @param normalizedNumber the normalized number of the given number. 951 * @param countryIso the ISO 3166-1 two letters country code, the country's 952 * convention will be used to format the number if the normalized 953 * phone is null. 954 * 955 * @return the formatted number, or the given number if it was formatted. 956 */ 957 private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { 958 if (TextUtils.isEmpty(number)) { 959 return ""; 960 } 961 // If "number" is really a SIP address, don't try to do any formatting at all. 962 if (PhoneNumberUtils.isUriNumber(number)) { 963 return number; 964 } 965 if (TextUtils.isEmpty(countryIso)) { 966 countryIso = mCurrentCountryIso; 967 } 968 return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); 969 } 970 971 private void resetNewCallsFlag() { 972 mCallLogQueryHandler.markNewCallsAsOld(); 973 } 974 975 private void startCallsQuery() { 976 mAdapter.setLoading(true); 977 mCallLogQueryHandler.fetchAllCalls(); 978 } 979 980 private void startVoicemailStatusQuery() { 981 mCallLogQueryHandler.fetchVoicemailStatus(); 982 } 983 984 @Override 985 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 986 super.onCreateOptionsMenu(menu, inflater); 987 inflater.inflate(R.menu.call_log_options, menu); 988 } 989 990 @Override 991 public void onPrepareOptionsMenu(Menu menu) { 992 menu.findItem(R.id.delete_all).setVisible(mShowOptionsMenu); 993 menu.findItem(R.id.show_voicemails_only).setVisible(mShowOptionsMenu); 994 final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_call_log); 995 if (mShowOptionsMenu) { 996 callSettingsMenuItem.setVisible(true); 997 callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent()); 998 } else { 999 callSettingsMenuItem.setVisible(false); 1000 } 1001 } 1002 1003 @Override 1004 public boolean onOptionsItemSelected(MenuItem item) { 1005 switch (item.getItemId()) { 1006 case R.id.delete_all: { 1007 ClearCallLogDialog.show(getFragmentManager()); 1008 return true; 1009 } 1010 1011 case R.id.show_voicemails_only: { 1012 mCallLogQueryHandler.fetchVoicemailOnly(); 1013 return true; 1014 } 1015 default: 1016 return false; 1017 } 1018 } 1019 1020 /* 1021 * Get the number from the Contacts, if available, since sometimes 1022 * the number provided by caller id may not be formatted properly 1023 * depending on the carrier (roaming) in use at the time of the 1024 * incoming call. 1025 * Logic : If the caller-id number starts with a "+", use it 1026 * Else if the number in the contacts starts with a "+", use that one 1027 * Else if the number in the contacts is longer, use that one 1028 */ 1029 private String getBetterNumberFromContacts(String number) { 1030 String matchingNumber = null; 1031 // Look in the cache first. If it's not found then query the Phones db 1032 ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number); 1033 if (ci != null && ci != ContactInfo.EMPTY) { 1034 matchingNumber = ci.number; 1035 } else { 1036 try { 1037 Cursor phonesCursor = getActivity().getContentResolver().query( 1038 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 1039 PhoneQuery._PROJECTION, null, null, null); 1040 if (phonesCursor != null) { 1041 if (phonesCursor.moveToFirst()) { 1042 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 1043 } 1044 phonesCursor.close(); 1045 } 1046 } catch (Exception e) { 1047 // Use the number from the call log 1048 } 1049 } 1050 if (!TextUtils.isEmpty(matchingNumber) && 1051 (matchingNumber.startsWith("+") 1052 || matchingNumber.length() > number.length())) { 1053 number = matchingNumber; 1054 } 1055 return number; 1056 } 1057 1058 public void callSelectedEntry() { 1059 int position = getListView().getSelectedItemPosition(); 1060 if (position < 0) { 1061 // In touch mode you may often not have something selected, so 1062 // just call the first entry to make sure that [send] [send] calls the 1063 // most recent entry. 1064 position = 0; 1065 } 1066 final Cursor cursor = (Cursor)mAdapter.getItem(position); 1067 if (cursor != null) { 1068 String number = cursor.getString(CallLogQuery.NUMBER); 1069 if (TextUtils.isEmpty(number) 1070 || number.equals(CallerInfo.UNKNOWN_NUMBER) 1071 || number.equals(CallerInfo.PRIVATE_NUMBER) 1072 || number.equals(CallerInfo.PAYPHONE_NUMBER)) { 1073 // This number can't be called, do nothing 1074 return; 1075 } 1076 Intent intent; 1077 // If "number" is really a SIP address, construct a sip: URI. 1078 if (PhoneNumberUtils.isUriNumber(number)) { 1079 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1080 Uri.fromParts("sip", number, null)); 1081 } else { 1082 // We're calling a regular PSTN phone number. 1083 // Construct a tel: URI, but do some other possible cleanup first. 1084 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 1085 if (!number.startsWith("+") && 1086 (callType == Calls.INCOMING_TYPE 1087 || callType == Calls.MISSED_TYPE)) { 1088 // If the caller-id matches a contact with a better qualified number, use it 1089 number = getBetterNumberFromContacts(number); 1090 } 1091 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1092 Uri.fromParts("tel", number, null)); 1093 } 1094 intent.setFlags( 1095 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 1096 startActivity(intent); 1097 } 1098 } 1099 1100 @Override 1101 public void onListItemClick(ListView l, View v, int position, long id) { 1102 Cursor cursor = (Cursor) mAdapter.getItem(position); 1103 if (CallLogQuery.isSectionHeader(cursor)) { 1104 // Do nothing when a header is clicked. 1105 return; 1106 } 1107 Intent intent = new Intent(getActivity(), CallDetailActivity.class); 1108 if (mAdapter.isGroupHeader(position)) { 1109 // We want to restore the position in the cursor at the end. 1110 int currentPosition = cursor.getPosition(); 1111 int groupSize = mAdapter.getGroupSize(position); 1112 long[] ids = new long[groupSize]; 1113 // Copy the ids of the rows in the group. 1114 for (int index = 0; index < groupSize; ++index) { 1115 ids[index] = cursor.getLong(CallLogQuery.ID); 1116 cursor.moveToNext(); 1117 } 1118 intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, ids); 1119 cursor.moveToPosition(currentPosition); 1120 } else { 1121 // If there is a single item, use the direct URI for it. 1122 intent.setData(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, id)); 1123 String voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); 1124 if (voicemailUri != null) { 1125 intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri)); 1126 } 1127 intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false); 1128 } 1129 startActivity(intent); 1130 } 1131 1132 @VisibleForTesting 1133 public CallLogAdapter getAdapter() { 1134 return mAdapter; 1135 } 1136 1137 @VisibleForTesting 1138 public String getVoiceMailNumber() { 1139 return mVoiceMailNumber; 1140 } 1141 1142 @Override 1143 public void onVisibilityChanged(boolean visible) { 1144 mShowOptionsMenu = visible; 1145 if (visible && isResumed()) { 1146 refreshData(); 1147 } 1148 } 1149 1150 /** Requests updates to the data to be shown. */ 1151 private void refreshData() { 1152 // Mark all entries in the contact info cache as out of date, so they will be looked up 1153 // again once being shown. 1154 mAdapter.invalidateCache(); 1155 startCallsQuery(); 1156 resetNewCallsFlag(); 1157 startVoicemailStatusQuery(); 1158 mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw 1159 // Clear notifications only when window gains focus. This activity won't 1160 // immediately receive focus if the keyguard screen is above it. 1161 if (getActivity().hasWindowFocus()) { 1162 removeMissedCallNotifications(); 1163 } 1164 } 1165 1166 /** Removes the missed call notifications. */ 1167 private void removeMissedCallNotifications() { 1168 try { 1169 ITelephony telephony = 1170 ITelephony.Stub.asInterface(ServiceManager.getService("phone")); 1171 if (telephony != null) { 1172 telephony.cancelMissedCallsNotification(); 1173 } else { 1174 Log.w(TAG, "Telephony service is null, can't call " + 1175 "cancelMissedCallsNotification"); 1176 } 1177 } catch (RemoteException e) { 1178 Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); 1179 } 1180 } 1181} 1182