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