CallLogFragment.java revision 438e1f19279ea058e145c84dbe3ba62cc14df645
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.R; 24import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener; 25import com.android.contacts.util.ExpirableCache; 26import com.android.internal.telephony.CallerInfo; 27import com.google.common.annotations.VisibleForTesting; 28 29import android.app.ListFragment; 30import android.content.AsyncQueryHandler; 31import android.content.ContentUris; 32import android.content.ContentValues; 33import android.content.Context; 34import android.content.Intent; 35import android.database.CharArrayBuffer; 36import android.database.Cursor; 37import android.database.sqlite.SQLiteDatabaseCorruptException; 38import android.database.sqlite.SQLiteDiskIOException; 39import android.database.sqlite.SQLiteException; 40import android.database.sqlite.SQLiteFullException; 41import android.graphics.drawable.Drawable; 42import android.net.Uri; 43import android.os.Bundle; 44import android.os.Handler; 45import android.os.Looper; 46import android.os.Message; 47import android.provider.CallLog; 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.Intents.Insert; 53import android.provider.ContactsContract.PhoneLookup; 54import android.telephony.PhoneNumberUtils; 55import android.telephony.TelephonyManager; 56import android.text.TextUtils; 57import android.util.Log; 58import android.view.ContextMenu; 59import android.view.ContextMenu.ContextMenuInfo; 60import android.view.LayoutInflater; 61import android.view.Menu; 62import android.view.MenuInflater; 63import android.view.MenuItem; 64import android.view.View; 65import android.view.ViewGroup; 66import android.view.ViewTreeObserver; 67import android.widget.AdapterView; 68import android.widget.ImageView; 69import android.widget.ListView; 70import android.widget.TextView; 71 72import java.lang.ref.WeakReference; 73import java.util.LinkedList; 74 75/** 76 * Displays a list of call log entries. 77 */ 78public class CallLogFragment extends ListFragment 79 implements View.OnCreateContextMenuListener, ViewPagerVisibilityListener { 80 private static final String TAG = "CallLogFragment"; 81 82 /** 83 * The size of the cache of contact info. 84 */ 85 private static final int CONTACT_INFO_CACHE_SIZE = 100; 86 87 /** The query for the call log table */ 88 private static final class CallLogQuery { 89 public static final String[] _PROJECTION = new String[] { 90 Calls._ID, 91 Calls.NUMBER, 92 Calls.DATE, 93 Calls.DURATION, 94 Calls.TYPE, 95 Calls.CACHED_NAME, 96 Calls.CACHED_NUMBER_TYPE, 97 Calls.CACHED_NUMBER_LABEL, 98 Calls.COUNTRY_ISO}; 99 100 public static final int ID = 0; 101 public static final int NUMBER = 1; 102 public static final int DATE = 2; 103 public static final int DURATION = 3; 104 public static final int CALL_TYPE = 4; 105 public static final int CALLER_NAME = 5; 106 public static final int CALLER_NUMBERTYPE = 6; 107 public static final int CALLER_NUMBERLABEL = 7; 108 public static final int COUNTRY_ISO = 8; 109 } 110 111 /** The query to use for the phones table */ 112 private static final class PhoneQuery { 113 public static final String[] _PROJECTION = new String[] { 114 PhoneLookup._ID, 115 PhoneLookup.DISPLAY_NAME, 116 PhoneLookup.TYPE, 117 PhoneLookup.LABEL, 118 PhoneLookup.NUMBER, 119 PhoneLookup.NORMALIZED_NUMBER, 120 PhoneLookup.PHOTO_ID}; 121 122 public static final int PERSON_ID = 0; 123 public static final int NAME = 1; 124 public static final int PHONE_TYPE = 2; 125 public static final int LABEL = 3; 126 public static final int MATCHED_NUMBER = 4; 127 public static final int NORMALIZED_NUMBER = 5; 128 public static final int PHOTO_ID = 6; 129 } 130 131 private static final class MenuItems { 132 public static final int DELETE = 1; 133 } 134 135 private static final class OptionsMenuItems { 136 public static final int DELETE_ALL = 1; 137 } 138 139 private static final int QUERY_TOKEN = 53; 140 private static final int UPDATE_TOKEN = 54; 141 142 private CallLogAdapter mAdapter; 143 private QueryHandler mQueryHandler; 144 private String mVoiceMailNumber; 145 private String mCurrentCountryIso; 146 private boolean mScrollToTop; 147 148 private MenuItem mDeleteAllCallLogMenuItem; 149 private boolean mShowMenu; 150 151 public static final class ContactInfo { 152 public long personId; 153 public String name; 154 public int type; 155 public String label; 156 public String number; 157 public String formattedNumber; 158 public String normalizedNumber; 159 public long photoId; 160 161 public static ContactInfo EMPTY = new ContactInfo(); 162 } 163 164 public static final class CallerInfoQuery { 165 public String number; 166 public int position; 167 public String name; 168 public int numberType; 169 public String numberLabel; 170 public long photoId; 171 } 172 173 /** Adapter class to fill in data for the Call Log */ 174 public final class CallLogAdapter extends GroupingListAdapter 175 implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener { 176 ExpirableCache<String, ContactInfo> mContactInfoCache; 177 private final LinkedList<CallerInfoQuery> mRequests; 178 private volatile boolean mDone; 179 private boolean mLoading = true; 180 ViewTreeObserver.OnPreDrawListener mPreDrawListener; 181 private static final int REDRAW = 1; 182 private static final int START_THREAD = 2; 183 private boolean mFirst; 184 private Thread mCallerIdThread; 185 186 /** Instance of helper class for managing views. */ 187 private final CallLogListItemHelper mCallLogViewsHelper; 188 189 /** 190 * Reusable char array buffers. 191 */ 192 private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128); 193 private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128); 194 /** Helper to set up contact photos. */ 195 private final ContactPhotoManager mContactPhotoManager; 196 197 @Override 198 public void onClick(View view) { 199 String number = (String) view.getTag(); 200 if (!TextUtils.isEmpty(number)) { 201 // Here, "number" can either be a PSTN phone number or a 202 // SIP address. So turn it into either a tel: URI or a 203 // sip: URI, as appropriate. 204 Uri callUri; 205 if (PhoneNumberUtils.isUriNumber(number)) { 206 callUri = Uri.fromParts("sip", number, null); 207 } else { 208 callUri = Uri.fromParts("tel", number, null); 209 } 210 startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri)); 211 } 212 } 213 214 @Override 215 public boolean onPreDraw() { 216 if (mFirst) { 217 mHandler.sendEmptyMessageDelayed(START_THREAD, 1000); 218 mFirst = false; 219 } 220 return true; 221 } 222 223 private Handler mHandler = new Handler() { 224 @Override 225 public void handleMessage(Message msg) { 226 switch (msg.what) { 227 case REDRAW: 228 notifyDataSetChanged(); 229 break; 230 case START_THREAD: 231 startRequestProcessing(); 232 break; 233 } 234 } 235 }; 236 237 public CallLogAdapter() { 238 super(getActivity()); 239 240 mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 241 mRequests = new LinkedList<CallerInfoQuery>(); 242 mPreDrawListener = null; 243 244 Drawable drawableIncoming = getResources().getDrawable( 245 R.drawable.ic_call_log_list_incoming_call); 246 Drawable drawableOutgoing = getResources().getDrawable( 247 R.drawable.ic_call_log_list_outgoing_call); 248 Drawable drawableMissed = getResources().getDrawable( 249 R.drawable.ic_call_log_list_missed_call); 250 251 mContactPhotoManager = ContactPhotoManager.getInstance(getActivity()); 252 mCallLogViewsHelper = new CallLogListItemHelper(getResources(), mVoiceMailNumber, 253 drawableIncoming, drawableOutgoing, drawableMissed); 254 } 255 256 /** 257 * Requery on background thread when {@link Cursor} changes. 258 */ 259 @Override 260 protected void onContentChanged() { 261 // Start async requery 262 startQuery(); 263 } 264 265 void setLoading(boolean loading) { 266 mLoading = loading; 267 } 268 269 @Override 270 public boolean isEmpty() { 271 if (mLoading) { 272 // We don't want the empty state to show when loading. 273 return false; 274 } else { 275 return super.isEmpty(); 276 } 277 } 278 279 public ContactInfo getContactInfo(String number) { 280 return mContactInfoCache.getPossiblyExpired(number); 281 } 282 283 public void startRequestProcessing() { 284 mDone = false; 285 mCallerIdThread = new Thread(this); 286 mCallerIdThread.setPriority(Thread.MIN_PRIORITY); 287 mCallerIdThread.start(); 288 } 289 290 /** 291 * Stops the background thread that processes updates and cancels any pending requests to 292 * start it. 293 * <p> 294 * Should be called from the main thread to prevent a race condition between the request to 295 * start the thread being processed and stopping the thread. 296 */ 297 public void stopRequestProcessing() { 298 // Remove any pending requests to start the processing thread. 299 mHandler.removeMessages(START_THREAD); 300 mDone = true; 301 if (mCallerIdThread != null) mCallerIdThread.interrupt(); 302 } 303 304 public void invalidateCache() { 305 mContactInfoCache.expireAll(); 306 } 307 308 private void updateCallLog(CallerInfoQuery ciq, ContactInfo ci) { 309 // Check if they are different. If not, don't update. 310 if (TextUtils.equals(ciq.name, ci.name) 311 && TextUtils.equals(ciq.numberLabel, ci.label) 312 && ciq.numberType == ci.type 313 && ciq.photoId == ci.photoId) { 314 return; 315 } 316 ContentValues values = new ContentValues(3); 317 values.put(Calls.CACHED_NAME, ci.name); 318 values.put(Calls.CACHED_NUMBER_TYPE, ci.type); 319 values.put(Calls.CACHED_NUMBER_LABEL, ci.label); 320 321 try { 322 getActivity().getContentResolver().update(Calls.CONTENT_URI, values, 323 Calls.NUMBER + "='" + ciq.number + "'", null); 324 } catch (SQLiteDiskIOException e) { 325 Log.w(TAG, "Exception while updating call info", e); 326 } catch (SQLiteFullException e) { 327 Log.w(TAG, "Exception while updating call info", e); 328 } catch (SQLiteDatabaseCorruptException e) { 329 Log.w(TAG, "Exception while updating call info", e); 330 } 331 } 332 333 private void enqueueRequest(String number, int position, 334 String name, int numberType, String numberLabel, long photoId) { 335 CallerInfoQuery ciq = new CallerInfoQuery(); 336 ciq.number = number; 337 ciq.position = position; 338 ciq.name = name; 339 ciq.numberType = numberType; 340 ciq.numberLabel = numberLabel; 341 ciq.photoId = photoId; 342 synchronized (mRequests) { 343 mRequests.add(ciq); 344 mRequests.notifyAll(); 345 } 346 } 347 348 private boolean queryContactInfo(CallerInfoQuery ciq) { 349 // First check if there was a prior request for the same number 350 // that was already satisfied 351 ContactInfo info = mContactInfoCache.get(ciq.number); 352 boolean needNotify = false; 353 if (info != null && info != ContactInfo.EMPTY) { 354 return true; 355 } else { 356 // Ok, do a fresh Contacts lookup for ciq.number. 357 boolean infoUpdated = false; 358 359 if (PhoneNumberUtils.isUriNumber(ciq.number)) { 360 // This "number" is really a SIP address. 361 362 // TODO: This code is duplicated from the 363 // CallerInfoAsyncQuery class. To avoid that, could the 364 // code here just use CallerInfoAsyncQuery, rather than 365 // manually running ContentResolver.query() itself? 366 367 // We look up SIP addresses directly in the Data table: 368 Uri contactRef = Data.CONTENT_URI; 369 370 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. 371 // 372 // Also note we use "upper(data1)" in the WHERE clause, and 373 // uppercase the incoming SIP address, in order to do a 374 // case-insensitive match. 375 // 376 // TODO: May also need to normalize by adding "sip:" as a 377 // prefix, if we start storing SIP addresses that way in the 378 // database. 379 String selection = "upper(" + Data.DATA1 + ")=?" 380 + " AND " 381 + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'"; 382 String[] selectionArgs = new String[] { ciq.number.toUpperCase() }; 383 384 Cursor dataTableCursor = 385 getActivity().getContentResolver().query( 386 contactRef, 387 null, // projection 388 selection, // selection 389 selectionArgs, // selectionArgs 390 null); // sortOrder 391 392 if (dataTableCursor != null) { 393 if (dataTableCursor.moveToFirst()) { 394 info = new ContactInfo(); 395 396 // TODO: we could slightly speed this up using an 397 // explicit projection (and thus not have to do 398 // those getColumnIndex() calls) but the benefit is 399 // very minimal. 400 401 // Note the Data.CONTACT_ID column here is 402 // equivalent to the PERSON_ID_COLUMN_INDEX column 403 // we use with "phonesCursor" below. 404 info.personId = dataTableCursor.getLong( 405 dataTableCursor.getColumnIndex(Data.CONTACT_ID)); 406 info.name = dataTableCursor.getString( 407 dataTableCursor.getColumnIndex(Data.DISPLAY_NAME)); 408 // "type" and "label" are currently unused for SIP addresses 409 info.type = SipAddress.TYPE_OTHER; 410 info.label = null; 411 412 // And "number" is the SIP address. 413 // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent. 414 info.number = dataTableCursor.getString( 415 dataTableCursor.getColumnIndex(Data.DATA1)); 416 info.normalizedNumber = null; // meaningless for SIP addresses 417 info.photoId = dataTableCursor.getLong( 418 dataTableCursor.getColumnIndex(Data.PHOTO_ID)); 419 420 infoUpdated = true; 421 } 422 dataTableCursor.close(); 423 } 424 } else { 425 // "number" is a regular phone number, so use the 426 // PhoneLookup table: 427 Cursor phonesCursor = 428 getActivity().getContentResolver().query( 429 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 430 Uri.encode(ciq.number)), 431 PhoneQuery._PROJECTION, null, null, null); 432 if (phonesCursor != null) { 433 if (phonesCursor.moveToFirst()) { 434 info = new ContactInfo(); 435 info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID); 436 info.name = phonesCursor.getString(PhoneQuery.NAME); 437 info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE); 438 info.label = phonesCursor.getString(PhoneQuery.LABEL); 439 info.number = phonesCursor 440 .getString(PhoneQuery.MATCHED_NUMBER); 441 info.normalizedNumber = phonesCursor 442 .getString(PhoneQuery.NORMALIZED_NUMBER); 443 info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID); 444 445 infoUpdated = true; 446 } 447 phonesCursor.close(); 448 } 449 } 450 451 if (infoUpdated) { 452 // New incoming phone number invalidates our formatted 453 // cache. Any cache fills happen only on the GUI thread. 454 info.formattedNumber = null; 455 456 mContactInfoCache.put(ciq.number, info); 457 458 // Inform list to update this item, if in view 459 needNotify = true; 460 } 461 } 462 if (info != null) { 463 updateCallLog(ciq, info); 464 } 465 return needNotify; 466 } 467 468 /* 469 * Handles requests for contact name and number type 470 * @see java.lang.Runnable#run() 471 */ 472 @Override 473 public void run() { 474 boolean needNotify = false; 475 while (!mDone) { 476 CallerInfoQuery ciq = null; 477 synchronized (mRequests) { 478 if (!mRequests.isEmpty()) { 479 ciq = mRequests.removeFirst(); 480 } else { 481 if (needNotify) { 482 needNotify = false; 483 mHandler.sendEmptyMessage(REDRAW); 484 } 485 try { 486 mRequests.wait(1000); 487 } catch (InterruptedException ie) { 488 // Ignore and continue processing requests 489 Thread.currentThread().interrupt(); 490 } 491 } 492 } 493 if (!mDone && ciq != null && queryContactInfo(ciq)) { 494 needNotify = true; 495 } 496 } 497 } 498 499 @Override 500 protected void addGroups(Cursor cursor) { 501 502 int count = cursor.getCount(); 503 if (count == 0) { 504 return; 505 } 506 507 int groupItemCount = 1; 508 509 CharArrayBuffer currentValue = mBuffer1; 510 CharArrayBuffer value = mBuffer2; 511 cursor.moveToFirst(); 512 cursor.copyStringToBuffer(CallLogQuery.NUMBER, currentValue); 513 int currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE); 514 for (int i = 1; i < count; i++) { 515 cursor.moveToNext(); 516 cursor.copyStringToBuffer(CallLogQuery.NUMBER, value); 517 boolean sameNumber = equalPhoneNumbers(value, currentValue); 518 519 // Group adjacent calls with the same number. Make an exception 520 // for the latest item if it was a missed call. We don't want 521 // a missed call to be hidden inside a group. 522 if (sameNumber && currentCallType != Calls.MISSED_TYPE) { 523 groupItemCount++; 524 } else { 525 if (groupItemCount > 1) { 526 addGroup(i - groupItemCount, groupItemCount, false); 527 } 528 529 groupItemCount = 1; 530 531 // Swap buffers 532 CharArrayBuffer temp = currentValue; 533 currentValue = value; 534 value = temp; 535 536 // If we have just examined a row following a missed call, make 537 // sure that it is grouped with subsequent calls from the same number 538 // even if it was also missed. 539 if (sameNumber && currentCallType == Calls.MISSED_TYPE) { 540 currentCallType = 0; // "not a missed call" 541 } else { 542 currentCallType = cursor.getInt(CallLogQuery.CALL_TYPE); 543 } 544 } 545 } 546 if (groupItemCount > 1) { 547 addGroup(count - groupItemCount, groupItemCount, false); 548 } 549 } 550 551 protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { 552 553 // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid 554 // string allocation 555 return PhoneNumberUtils.compare(new String(buffer1.data, 0, buffer1.sizeCopied), 556 new String(buffer2.data, 0, buffer2.sizeCopied)); 557 } 558 559 560 @VisibleForTesting 561 @Override 562 public View newStandAloneView(Context context, ViewGroup parent) { 563 LayoutInflater inflater = 564 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 565 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 566 findAndCacheViews(view); 567 return view; 568 } 569 570 @VisibleForTesting 571 @Override 572 public void bindStandAloneView(View view, Context context, Cursor cursor) { 573 bindView(context, view, cursor); 574 } 575 576 @VisibleForTesting 577 @Override 578 public View newChildView(Context context, ViewGroup parent) { 579 LayoutInflater inflater = 580 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 581 View view = inflater.inflate(R.layout.call_log_list_child_item, parent, false); 582 findAndCacheViews(view); 583 return view; 584 } 585 586 @VisibleForTesting 587 @Override 588 public void bindChildView(View view, Context context, Cursor cursor) { 589 bindView(context, view, cursor); 590 } 591 592 @VisibleForTesting 593 @Override 594 public View newGroupView(Context context, ViewGroup parent) { 595 LayoutInflater inflater = 596 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 597 View view = inflater.inflate(R.layout.call_log_list_group_item, parent, false); 598 findAndCacheViews(view); 599 return view; 600 } 601 602 @VisibleForTesting 603 @Override 604 public void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 605 boolean expanded) { 606 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 607 int groupIndicator = expanded 608 ? com.android.internal.R.drawable.expander_ic_maximized 609 : com.android.internal.R.drawable.expander_ic_minimized; 610 views.groupIndicator.setImageResource(groupIndicator); 611 views.groupSize.setText("(" + groupSize + ")"); 612 bindView(context, view, cursor); 613 } 614 615 private void findAndCacheViews(View view) { 616 617 // Get the views to bind to 618 CallLogListItemViews views = new CallLogListItemViews(); 619 views.line1View = (TextView) view.findViewById(R.id.line1); 620 views.labelView = (TextView) view.findViewById(R.id.label); 621 views.numberView = (TextView) view.findViewById(R.id.number); 622 views.dateView = (TextView) view.findViewById(R.id.date); 623 views.iconView = (ImageView) view.findViewById(R.id.call_type_icon); 624 views.callView = view.findViewById(R.id.call_icon); 625 if (views.callView != null) { 626 views.callView.setOnClickListener(this); 627 } 628 views.groupIndicator = (ImageView) view.findViewById(R.id.groupIndicator); 629 views.groupSize = (TextView) view.findViewById(R.id.groupSize); 630 views.photoView = (ImageView) view.findViewById(R.id.contact_photo); 631 view.setTag(views); 632 } 633 634 public void bindView(Context context, View view, Cursor c) { 635 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 636 637 String number = c.getString(CallLogQuery.NUMBER); 638 String formattedNumber = null; 639 String callerName = c.getString(CallLogQuery.CALLER_NAME); 640 int callerNumberType = c.getInt(CallLogQuery.CALLER_NUMBERTYPE); 641 String callerNumberLabel = c.getString(CallLogQuery.CALLER_NUMBERLABEL); 642 String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 643 // Store away the number so we can call it directly if you click on the call icon 644 if (views.callView != null) { 645 views.callView.setTag(number); 646 } 647 648 // Lookup contacts with this number 649 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 650 mContactInfoCache.getCachedValue(number); 651 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 652 if (cachedInfo == null) { 653 // Mark it as empty and queue up a request to find the name 654 // The db request should happen on a non-UI thread 655 info = ContactInfo.EMPTY; 656 mContactInfoCache.put(number, info); 657 Log.d(TAG, "Contact info missing: " + number); 658 enqueueRequest(number, c.getPosition(), 659 callerName, callerNumberType, callerNumberLabel, 0L); 660 } else if (info != ContactInfo.EMPTY) { // Has been queried 661 // Check if any data is different from the data cached in the 662 // calls db. If so, queue the request so that we can update 663 // the calls db. 664 if (!TextUtils.equals(info.name, callerName) 665 || info.type != callerNumberType 666 || !TextUtils.equals(info.label, callerNumberLabel)) { 667 // Something is amiss, so sync up. 668 Log.w(TAG, "Contact info inconsistent: " + number); 669 enqueueRequest(number, c.getPosition(), 670 callerName, callerNumberType, callerNumberLabel, info.photoId); 671 } else if (cachedInfo.isExpired()) { 672 Log.d(TAG, "Contact info expired: " + number); 673 // Put it back in the cache, therefore marking it as not expired, so that other 674 // entries with the same number will not re-request it. 675 mContactInfoCache.put(number, info); 676 // The contact info is no longer up to date, we should request it. 677 enqueueRequest(number, c.getPosition(), info.name, info.type, info.label, 678 info.photoId); 679 } 680 681 // Format and cache phone number for found contact 682 if (info.formattedNumber == null) { 683 info.formattedNumber = 684 formatPhoneNumber(info.number, info.normalizedNumber, countryIso); 685 } 686 formattedNumber = info.formattedNumber; 687 } 688 689 String name = info.name; 690 int ntype = info.type; 691 String label = info.label; 692 long photoId = info.photoId; 693 // If there's no name cached in our hashmap, but there's one in the 694 // calls db, use the one in the calls db. Otherwise the name in our 695 // hashmap is more recent, so it has precedence. 696 if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(callerName)) { 697 name = callerName; 698 ntype = callerNumberType; 699 label = callerNumberLabel; 700 701 // Format the cached call_log phone number 702 formattedNumber = formatPhoneNumber(number, null, countryIso); 703 } 704 705 // Assumes the call back feature is on most of the 706 // time. For private and unknown numbers: hide it. 707 if (views.callView != null) { 708 views.callView.setVisibility(View.VISIBLE); 709 } 710 711 if (!TextUtils.isEmpty(name)) { 712 mCallLogViewsHelper.setContactNameLabelAndNumber(views, name, number, ntype, label, 713 formattedNumber); 714 } else { 715 // TODO: Do we need to format the number again? Is formattedNumber already storing 716 // this value? 717 mCallLogViewsHelper.setContactNumberOnly(views, number, 718 formatPhoneNumber(number, null, countryIso)); 719 } 720 mCallLogViewsHelper.setDate(views, c.getLong(CallLogQuery.DATE), 721 System.currentTimeMillis()); 722 mCallLogViewsHelper.setCallType(views, c.getInt(CallLogQuery.CALL_TYPE)); 723 if (views.photoView != null) { 724 mContactPhotoManager.loadPhoto(views.photoView, photoId); 725 } 726 727 728 // Listen for the first draw 729 if (mPreDrawListener == null) { 730 mFirst = true; 731 mPreDrawListener = this; 732 view.getViewTreeObserver().addOnPreDrawListener(this); 733 } 734 } 735 } 736 737 private static final class QueryHandler extends AsyncQueryHandler { 738 private final WeakReference<CallLogFragment> mFragment; 739 740 /** 741 * Simple handler that wraps background calls to catch 742 * {@link SQLiteException}, such as when the disk is full. 743 */ 744 protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { 745 public CatchingWorkerHandler(Looper looper) { 746 super(looper); 747 } 748 749 @Override 750 public void handleMessage(Message msg) { 751 try { 752 // Perform same query while catching any exceptions 753 super.handleMessage(msg); 754 } catch (SQLiteDiskIOException e) { 755 Log.w(TAG, "Exception on background worker thread", e); 756 } catch (SQLiteFullException e) { 757 Log.w(TAG, "Exception on background worker thread", e); 758 } catch (SQLiteDatabaseCorruptException e) { 759 Log.w(TAG, "Exception on background worker thread", e); 760 } 761 } 762 } 763 764 @Override 765 protected Handler createHandler(Looper looper) { 766 // Provide our special handler that catches exceptions 767 return new CatchingWorkerHandler(looper); 768 } 769 770 public QueryHandler(CallLogFragment fragment) { 771 super(fragment.getActivity().getContentResolver()); 772 mFragment = new WeakReference<CallLogFragment>(fragment); 773 } 774 775 @Override 776 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 777 final CallLogFragment fragment = mFragment.get(); 778 if (fragment != null && fragment.getActivity() != null && 779 !fragment.getActivity().isFinishing()) { 780 final CallLogFragment.CallLogAdapter callsAdapter = fragment.mAdapter; 781 callsAdapter.setLoading(false); 782 callsAdapter.changeCursor(cursor); 783 if (fragment.mScrollToTop) { 784 final ListView listView = fragment.getListView(); 785 if (listView.getFirstVisiblePosition() > 5) { 786 listView.setSelection(5); 787 } 788 listView.smoothScrollToPosition(0); 789 fragment.mScrollToTop = false; 790 } 791 } else { 792 cursor.close(); 793 } 794 } 795 } 796 797 @Override 798 public void onCreate(Bundle state) { 799 super.onCreate(state); 800 801 mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService( 802 Context.TELEPHONY_SERVICE)).getVoiceMailNumber(); 803 mQueryHandler = new QueryHandler(this); 804 805 mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); 806 807 setHasOptionsMenu(true); 808 } 809 810 @Override 811 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 812 return inflater.inflate(R.layout.call_log_fragment, container, false); 813 } 814 815 @Override 816 public void onViewCreated(View view, Bundle savedInstanceState) { 817 super.onViewCreated(view, savedInstanceState); 818 getListView().setOnCreateContextMenuListener(this); 819 mAdapter = new CallLogAdapter(); 820 setListAdapter(mAdapter); 821 } 822 823 @Override 824 public void onStart() { 825 mScrollToTop = true; 826 super.onStart(); 827 } 828 829 @Override 830 public void onResume() { 831 // Mark all entries in the contact info cache as out of date, so they will be looked up 832 // again once being shown. 833 if (mAdapter != null) { 834 mAdapter.invalidateCache(); 835 } 836 837 startQuery(); 838 resetNewCallsFlag(); 839 840 super.onResume(); 841 842 mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw 843 } 844 845 @Override 846 public void onPause() { 847 super.onPause(); 848 849 // Kill the requests thread 850 mAdapter.stopRequestProcessing(); 851 } 852 853 @Override 854 public void onDestroy() { 855 super.onDestroy(); 856 mAdapter.stopRequestProcessing(); 857 mAdapter.changeCursor(null); 858 } 859 860 /** 861 * Format the given phone number 862 * 863 * @param number the number to be formatted. 864 * @param normalizedNumber the normalized number of the given number. 865 * @param countryIso the ISO 3166-1 two letters country code, the country's 866 * convention will be used to format the number if the normalized 867 * phone is null. 868 * 869 * @return the formatted number, or the given number if it was formatted. 870 */ 871 private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { 872 if (TextUtils.isEmpty(number)) { 873 return ""; 874 } 875 // If "number" is really a SIP address, don't try to do any formatting at all. 876 if (PhoneNumberUtils.isUriNumber(number)) { 877 return number; 878 } 879 if (TextUtils.isEmpty(countryIso)) { 880 countryIso = mCurrentCountryIso; 881 } 882 return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); 883 } 884 885 private void resetNewCallsFlag() { 886 // Mark all "new" missed calls as not new anymore 887 StringBuilder where = new StringBuilder("type="); 888 where.append(Calls.MISSED_TYPE); 889 where.append(" AND new=1"); 890 891 ContentValues values = new ContentValues(1); 892 values.put(Calls.NEW, "0"); 893 mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI, 894 values, where.toString(), null); 895 } 896 897 private void startQuery() { 898 mAdapter.setLoading(true); 899 900 // Cancel any pending queries 901 mQueryHandler.cancelOperation(QUERY_TOKEN); 902 mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI, 903 CallLogQuery._PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER); 904 } 905 906 @Override 907 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 908 super.onCreateOptionsMenu(menu, inflater); 909 mDeleteAllCallLogMenuItem = menu.add(0, OptionsMenuItems.DELETE_ALL, 910 0, R.string.recentCalls_deleteAll) 911 .setIcon(android.R.drawable.ic_menu_close_clear_cancel); 912 } 913 914 @Override 915 public void onPrepareOptionsMenu(Menu menu) { 916 mDeleteAllCallLogMenuItem.setVisible(mShowMenu); 917 } 918 919 @Override 920 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 921 AdapterView.AdapterContextMenuInfo menuInfo; 922 try { 923 menuInfo = (AdapterView.AdapterContextMenuInfo) menuInfoIn; 924 } catch (ClassCastException e) { 925 Log.e(TAG, "bad menuInfoIn", e); 926 return; 927 } 928 929 Cursor cursor = (Cursor) mAdapter.getItem(menuInfo.position); 930 931 String number = cursor.getString(CallLogQuery.NUMBER); 932 Uri numberUri = null; 933 boolean isVoicemail = false; 934 boolean isSipNumber = false; 935 if (number.equals(CallerInfo.UNKNOWN_NUMBER)) { 936 number = getString(R.string.unknown); 937 } else if (number.equals(CallerInfo.PRIVATE_NUMBER)) { 938 number = getString(R.string.private_num); 939 } else if (number.equals(CallerInfo.PAYPHONE_NUMBER)) { 940 number = getString(R.string.payphone); 941 } else if (PhoneNumberUtils.extractNetworkPortion(number).equals(mVoiceMailNumber)) { 942 number = getString(R.string.voicemail); 943 numberUri = Uri.parse("voicemail:x"); 944 isVoicemail = true; 945 } else if (PhoneNumberUtils.isUriNumber(number)) { 946 numberUri = Uri.fromParts("sip", number, null); 947 isSipNumber = true; 948 } else { 949 numberUri = Uri.fromParts("tel", number, null); 950 } 951 952 ContactInfo info = mAdapter.getContactInfo(number); 953 boolean contactInfoPresent = (info != null && info != ContactInfo.EMPTY); 954 if (contactInfoPresent) { 955 menu.setHeaderTitle(info.name); 956 } else { 957 menu.setHeaderTitle(number); 958 } 959 960 if (numberUri != null) { 961 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, numberUri); 962 menu.add(0, 0, 0, getResources().getString(R.string.recentCalls_callNumber, number)) 963 .setIntent(intent); 964 } 965 966 if (contactInfoPresent) { 967 menu.add(0, 0, 0, R.string.menu_viewContact) 968 .setIntent(new Intent(Intent.ACTION_VIEW, 969 ContentUris.withAppendedId(Contacts.CONTENT_URI, info.personId))); 970 } 971 972 if (numberUri != null && !isVoicemail && !isSipNumber) { 973 menu.add(0, 0, 0, R.string.recentCalls_editNumberBeforeCall) 974 .setIntent(new Intent(Intent.ACTION_DIAL, numberUri)); 975 menu.add(0, 0, 0, R.string.menu_sendTextMessage) 976 .setIntent(new Intent(Intent.ACTION_SENDTO, 977 Uri.fromParts("sms", number, null))); 978 } 979 980 // "Add to contacts" item, if this entry isn't already associated with a contact 981 if (!contactInfoPresent && numberUri != null && !isVoicemail && !isSipNumber) { 982 // TODO: This item is currently disabled for SIP addresses, because 983 // the Insert.PHONE extra only works correctly for PSTN numbers. 984 // 985 // To fix this for SIP addresses, we need to: 986 // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if 987 // the current number is a SIP address 988 // - update the contacts UI code to handle Insert.SIP_ADDRESS by 989 // updating the SipAddress field 990 // and then we can remove the "!isSipNumber" check above. 991 992 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 993 intent.setType(Contacts.CONTENT_ITEM_TYPE); 994 intent.putExtra(Insert.PHONE, number); 995 menu.add(0, 0, 0, R.string.recentCalls_addToContact) 996 .setIntent(intent); 997 } 998 menu.add(0, MenuItems.DELETE, 0, R.string.recentCalls_removeFromRecentList); 999 } 1000 1001 @Override 1002 public boolean onOptionsItemSelected(MenuItem item) { 1003 switch (item.getItemId()) { 1004 case OptionsMenuItems.DELETE_ALL: { 1005 ClearCallLogDialog.show(getFragmentManager()); 1006 return true; 1007 } 1008 } 1009 return super.onOptionsItemSelected(item); 1010 } 1011 1012 @Override 1013 public boolean onContextItemSelected(MenuItem item) { 1014 // Convert the menu info to the proper type 1015 AdapterView.AdapterContextMenuInfo menuInfo; 1016 try { 1017 menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1018 } catch (ClassCastException e) { 1019 Log.e(TAG, "bad menuInfoIn", e); 1020 return false; 1021 } 1022 1023 switch (item.getItemId()) { 1024 case MenuItems.DELETE: { 1025 Cursor cursor = (Cursor)mAdapter.getItem(menuInfo.position); 1026 int groupSize = 1; 1027 if (mAdapter.isGroupHeader(menuInfo.position)) { 1028 groupSize = mAdapter.getGroupSize(menuInfo.position); 1029 } 1030 1031 StringBuilder sb = new StringBuilder(); 1032 for (int i = 0; i < groupSize; i++) { 1033 if (i != 0) { 1034 sb.append(","); 1035 cursor.moveToNext(); 1036 } 1037 long id = cursor.getLong(CallLogQuery.ID); 1038 sb.append(id); 1039 } 1040 1041 getActivity().getContentResolver().delete(Calls.CONTENT_URI, 1042 Calls._ID + " IN (" + sb + ")", null); 1043 } 1044 } 1045 return super.onContextItemSelected(item); 1046 } 1047 1048 /* 1049 * Get the number from the Contacts, if available, since sometimes 1050 * the number provided by caller id may not be formatted properly 1051 * depending on the carrier (roaming) in use at the time of the 1052 * incoming call. 1053 * Logic : If the caller-id number starts with a "+", use it 1054 * Else if the number in the contacts starts with a "+", use that one 1055 * Else if the number in the contacts is longer, use that one 1056 */ 1057 private String getBetterNumberFromContacts(String number) { 1058 String matchingNumber = null; 1059 // Look in the cache first. If it's not found then query the Phones db 1060 ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number); 1061 if (ci != null && ci != ContactInfo.EMPTY) { 1062 matchingNumber = ci.number; 1063 } else { 1064 try { 1065 Cursor phonesCursor = getActivity().getContentResolver().query( 1066 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 1067 PhoneQuery._PROJECTION, null, null, null); 1068 if (phonesCursor != null) { 1069 if (phonesCursor.moveToFirst()) { 1070 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 1071 } 1072 phonesCursor.close(); 1073 } 1074 } catch (Exception e) { 1075 // Use the number from the call log 1076 } 1077 } 1078 if (!TextUtils.isEmpty(matchingNumber) && 1079 (matchingNumber.startsWith("+") 1080 || matchingNumber.length() > number.length())) { 1081 number = matchingNumber; 1082 } 1083 return number; 1084 } 1085 1086 public void callSelectedEntry() { 1087 int position = getListView().getSelectedItemPosition(); 1088 if (position < 0) { 1089 // In touch mode you may often not have something selected, so 1090 // just call the first entry to make sure that [send] [send] calls the 1091 // most recent entry. 1092 position = 0; 1093 } 1094 final Cursor cursor = (Cursor)mAdapter.getItem(position); 1095 if (cursor != null) { 1096 String number = cursor.getString(CallLogQuery.NUMBER); 1097 if (TextUtils.isEmpty(number) 1098 || number.equals(CallerInfo.UNKNOWN_NUMBER) 1099 || number.equals(CallerInfo.PRIVATE_NUMBER) 1100 || number.equals(CallerInfo.PAYPHONE_NUMBER)) { 1101 // This number can't be called, do nothing 1102 return; 1103 } 1104 Intent intent; 1105 // If "number" is really a SIP address, construct a sip: URI. 1106 if (PhoneNumberUtils.isUriNumber(number)) { 1107 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1108 Uri.fromParts("sip", number, null)); 1109 } else { 1110 // We're calling a regular PSTN phone number. 1111 // Construct a tel: URI, but do some other possible cleanup first. 1112 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 1113 if (!number.startsWith("+") && 1114 (callType == Calls.INCOMING_TYPE 1115 || callType == Calls.MISSED_TYPE)) { 1116 // If the caller-id matches a contact with a better qualified number, use it 1117 number = getBetterNumberFromContacts(number); 1118 } 1119 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1120 Uri.fromParts("tel", number, null)); 1121 } 1122 intent.setFlags( 1123 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 1124 startActivity(intent); 1125 } 1126 } 1127 1128 @Override 1129 public void onListItemClick(ListView l, View v, int position, long id) { 1130 if (mAdapter.isGroupHeader(position)) { 1131 mAdapter.toggleGroup(position); 1132 } else { 1133 Intent intent = new Intent(getActivity(), CallDetailActivity.class); 1134 intent.setData(ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI, id)); 1135 startActivity(intent); 1136 } 1137 } 1138 1139 @VisibleForTesting 1140 public CallLogAdapter getAdapter() { 1141 return mAdapter; 1142 } 1143 1144 @VisibleForTesting 1145 public String getVoiceMailNumber() { 1146 return mVoiceMailNumber; 1147 } 1148 1149 @Override 1150 public void onVisibilityChange(boolean visible) { 1151 mShowMenu = visible; 1152 } 1153} 1154