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