CallLogFragment.java revision 719a7adde25e0a717816b00668c16c3a1e3c5518
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.dialer.calllog; 18 19import android.app.Activity; 20import android.app.KeyguardManager; 21import android.app.ListFragment; 22import android.content.Context; 23import android.content.Intent; 24import android.database.ContentObserver; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Bundle; 28import android.os.Handler; 29import android.os.RemoteException; 30import android.os.ServiceManager; 31import android.provider.CallLog; 32import android.provider.CallLog.Calls; 33import android.provider.ContactsContract; 34import android.telephony.PhoneNumberUtils; 35import android.telephony.PhoneStateListener; 36import android.telephony.TelephonyManager; 37import android.text.TextUtils; 38import android.util.Log; 39import android.view.LayoutInflater; 40import android.view.Menu; 41import android.view.MenuInflater; 42import android.view.MenuItem; 43import android.view.View; 44import android.view.ViewGroup; 45import android.widget.ListView; 46import android.widget.TextView; 47 48import com.android.common.io.MoreCloseables; 49import com.android.contacts.common.CallUtil; 50import com.android.contacts.common.GeoUtil; 51import com.android.dialer.R; 52import com.android.dialer.util.EmptyLoader; 53import com.android.dialer.voicemail.VoicemailStatusHelper; 54import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 55import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 56import com.android.internal.telephony.ITelephony; 57import com.google.common.annotations.VisibleForTesting; 58 59import java.util.List; 60 61/** 62 * Displays a list of call log entries. 63 */ 64public class CallLogFragment extends ListFragment 65 implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { 66 private static final String TAG = "CallLogFragment"; 67 68 /** 69 * ID of the empty loader to defer other fragments. 70 */ 71 private static final int EMPTY_LOADER_ID = 0; 72 73 private CallLogAdapter mAdapter; 74 private CallLogQueryHandler mCallLogQueryHandler; 75 private boolean mScrollToTop; 76 77 /** Whether there is at least one voicemail source installed. */ 78 private boolean mVoicemailSourcesAvailable = false; 79 80 private VoicemailStatusHelper mVoicemailStatusHelper; 81 private View mStatusMessageView; 82 private TextView mStatusMessageText; 83 private TextView mStatusMessageAction; 84 private TextView mFilterStatusView; 85 private KeyguardManager mKeyguardManager; 86 87 private boolean mEmptyLoaderRunning; 88 private boolean mCallLogFetched; 89 private boolean mVoicemailStatusFetched; 90 91 private final Handler mHandler = new Handler(); 92 93 private TelephonyManager mTelephonyManager; 94 private PhoneStateListener mPhoneStateListener; 95 96 private class CustomContentObserver extends ContentObserver { 97 public CustomContentObserver() { 98 super(mHandler); 99 } 100 @Override 101 public void onChange(boolean selfChange) { 102 mRefreshDataRequired = true; 103 } 104 } 105 106 // See issue 6363009 107 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 108 private final ContentObserver mContactsObserver = new CustomContentObserver(); 109 private boolean mRefreshDataRequired = true; 110 111 // Exactly same variable is in Fragment as a package private. 112 private boolean mMenuVisible = true; 113 114 // Default to all calls. 115 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 116 117 @Override 118 public void onCreate(Bundle state) { 119 super.onCreate(state); 120 121 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); 122 mKeyguardManager = 123 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 124 getActivity().getContentResolver().registerContentObserver( 125 CallLog.CONTENT_URI, true, mCallLogObserver); 126 getActivity().getContentResolver().registerContentObserver( 127 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); 128 setHasOptionsMenu(true); 129 } 130 131 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 132 @Override 133 public void onCallsFetched(Cursor cursor) { 134 if (getActivity() == null || getActivity().isFinishing()) { 135 return; 136 } 137 mAdapter.setLoading(false); 138 mAdapter.changeCursor(cursor); 139 // This will update the state of the "Clear call log" menu item. 140 getActivity().invalidateOptionsMenu(); 141 if (mScrollToTop) { 142 final ListView listView = getListView(); 143 // The smooth-scroll animation happens over a fixed time period. 144 // As a result, if it scrolls through a large portion of the list, 145 // each frame will jump so far from the previous one that the user 146 // will not experience the illusion of downward motion. Instead, 147 // if we're not already near the top of the list, we instantly jump 148 // near the top, and animate from there. 149 if (listView.getFirstVisiblePosition() > 5) { 150 listView.setSelection(5); 151 } 152 // Workaround for framework issue: the smooth-scroll doesn't 153 // occur if setSelection() is called immediately before. 154 mHandler.post(new Runnable() { 155 @Override 156 public void run() { 157 if (getActivity() == null || getActivity().isFinishing()) { 158 return; 159 } 160 listView.smoothScrollToPosition(0); 161 } 162 }); 163 164 mScrollToTop = false; 165 } 166 mCallLogFetched = true; 167 destroyEmptyLoaderIfAllDataFetched(); 168 } 169 170 /** 171 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 172 */ 173 @Override 174 public void onVoicemailStatusFetched(Cursor statusCursor) { 175 if (getActivity() == null || getActivity().isFinishing()) { 176 return; 177 } 178 updateVoicemailStatusMessage(statusCursor); 179 180 int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); 181 setVoicemailSourcesAvailable(activeSources != 0); 182 MoreCloseables.closeQuietly(statusCursor); 183 mVoicemailStatusFetched = true; 184 destroyEmptyLoaderIfAllDataFetched(); 185 } 186 187 private void destroyEmptyLoaderIfAllDataFetched() { 188 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 189 mEmptyLoaderRunning = false; 190 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 191 } 192 } 193 194 /** Sets whether there are any voicemail sources available in the platform. */ 195 private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { 196 if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; 197 mVoicemailSourcesAvailable = voicemailSourcesAvailable; 198 199 Activity activity = getActivity(); 200 if (activity != null) { 201 // This is so that the options menu content is updated. 202 activity.invalidateOptionsMenu(); 203 } 204 } 205 206 @Override 207 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 208 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 209 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 210 mStatusMessageView = view.findViewById(R.id.voicemail_status); 211 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 212 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 213 mFilterStatusView = (TextView) view.findViewById(R.id.filter_status); 214 return view; 215 } 216 217 @Override 218 public void onViewCreated(View view, Bundle savedInstanceState) { 219 super.onViewCreated(view, savedInstanceState); 220 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 221 mAdapter = new CallLogAdapter(getActivity(), this, 222 new ContactInfoHelper(getActivity(), currentCountryIso)); 223 setListAdapter(mAdapter); 224 getListView().setItemsCanFocus(true); 225 } 226 227 /** 228 * Based on the new intent, decide whether the list should be configured 229 * to scroll up to display the first item. 230 */ 231 public void configureScreenFromIntent(Intent newIntent) { 232 // Typically, when switching to the call-log we want to show the user 233 // the same section of the list that they were most recently looking 234 // at. However, under some circumstances, we want to automatically 235 // scroll to the top of the list to present the newest call items. 236 // For example, immediately after a call is finished, we want to 237 // display information about that call. 238 mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType()); 239 } 240 241 @Override 242 public void onStart() { 243 // Start the empty loader now to defer other fragments. We destroy it when both calllog 244 // and the voicemail status are fetched. 245 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 246 new EmptyLoader.Callback(getActivity())); 247 mEmptyLoaderRunning = true; 248 super.onStart(); 249 } 250 251 @Override 252 public void onResume() { 253 super.onResume(); 254 refreshData(); 255 } 256 257 private void updateVoicemailStatusMessage(Cursor statusCursor) { 258 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 259 if (messages.size() == 0) { 260 mStatusMessageView.setVisibility(View.GONE); 261 } else { 262 mStatusMessageView.setVisibility(View.VISIBLE); 263 // TODO: Change the code to show all messages. For now just pick the first message. 264 final StatusMessage message = messages.get(0); 265 if (message.showInCallLog()) { 266 mStatusMessageText.setText(message.callLogMessageId); 267 } 268 if (message.actionMessageId != -1) { 269 mStatusMessageAction.setText(message.actionMessageId); 270 } 271 if (message.actionUri != null) { 272 mStatusMessageAction.setVisibility(View.VISIBLE); 273 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 274 @Override 275 public void onClick(View v) { 276 getActivity().startActivity( 277 new Intent(Intent.ACTION_VIEW, message.actionUri)); 278 } 279 }); 280 } else { 281 mStatusMessageAction.setVisibility(View.GONE); 282 } 283 } 284 } 285 286 @Override 287 public void onPause() { 288 super.onPause(); 289 // Kill the requests thread 290 mAdapter.stopRequestProcessing(); 291 } 292 293 @Override 294 public void onStop() { 295 super.onStop(); 296 updateOnExit(); 297 } 298 299 @Override 300 public void onDestroy() { 301 super.onDestroy(); 302 mAdapter.stopRequestProcessing(); 303 mAdapter.changeCursor(null); 304 getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); 305 getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); 306 unregisterPhoneCallReceiver(); 307 } 308 309 @Override 310 public void fetchCalls() { 311 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 312 } 313 314 public void startCallsQuery() { 315 mAdapter.setLoading(true); 316 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 317 } 318 319 private void startVoicemailStatusQuery() { 320 mCallLogQueryHandler.fetchVoicemailStatus(); 321 } 322 323 @Override 324 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 325 super.onCreateOptionsMenu(menu, inflater); 326 inflater.inflate(R.menu.call_log_options, menu); 327 } 328 329 @Override 330 public void onPrepareOptionsMenu(Menu menu) { 331 final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); 332 // Check if all the menu items are inflated correctly. As a shortcut, we assume all 333 // menu items are ready if the first item is non-null. 334 if (itemDeleteAll != null) { 335 itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty()); 336 337 showAllFilterMenuOptions(menu); 338 hideCurrentFilterMenuOption(menu); 339 340 // Only hide if not available. Let the above calls handle showing. 341 if (!mVoicemailSourcesAvailable) { 342 menu.findItem(R.id.show_voicemails_only).setVisible(false); 343 } 344 } 345 } 346 347 private void hideCurrentFilterMenuOption(Menu menu) { 348 MenuItem item = null; 349 switch (mCallTypeFilter) { 350 case CallLogQueryHandler.CALL_TYPE_ALL: 351 item = menu.findItem(R.id.show_all_calls); 352 break; 353 case Calls.INCOMING_TYPE: 354 item = menu.findItem(R.id.show_incoming_only); 355 break; 356 case Calls.OUTGOING_TYPE: 357 item = menu.findItem(R.id.show_outgoing_only); 358 break; 359 case Calls.MISSED_TYPE: 360 item = menu.findItem(R.id.show_missed_only); 361 break; 362 case Calls.VOICEMAIL_TYPE: 363 menu.findItem(R.id.show_voicemails_only); 364 break; 365 } 366 if (item != null) { 367 item.setVisible(false); 368 } 369 } 370 371 private void showAllFilterMenuOptions(Menu menu) { 372 menu.findItem(R.id.show_all_calls).setVisible(true); 373 menu.findItem(R.id.show_incoming_only).setVisible(true); 374 menu.findItem(R.id.show_outgoing_only).setVisible(true); 375 menu.findItem(R.id.show_missed_only).setVisible(true); 376 menu.findItem(R.id.show_voicemails_only).setVisible(true); 377 } 378 379 @Override 380 public boolean onOptionsItemSelected(MenuItem item) { 381 switch (item.getItemId()) { 382 case R.id.delete_all: 383 ClearCallLogDialog.show(getFragmentManager()); 384 return true; 385 386 case R.id.show_outgoing_only: 387 // We only need the phone call receiver when there is an active call type filter. 388 // Not many people may use the filters so don't register the receiver until now . 389 registerPhoneCallReceiver(); 390 mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE); 391 updateFilterTypeAndHeader(Calls.OUTGOING_TYPE); 392 return true; 393 394 case R.id.show_incoming_only: 395 registerPhoneCallReceiver(); 396 mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE); 397 updateFilterTypeAndHeader(Calls.INCOMING_TYPE); 398 return true; 399 400 case R.id.show_missed_only: 401 registerPhoneCallReceiver(); 402 mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE); 403 updateFilterTypeAndHeader(Calls.MISSED_TYPE); 404 return true; 405 406 case R.id.show_voicemails_only: 407 registerPhoneCallReceiver(); 408 mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE); 409 updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE); 410 return true; 411 412 case R.id.show_all_calls: 413 // Filter is being turned off, receiver no longer needed. 414 unregisterPhoneCallReceiver(); 415 mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); 416 updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); 417 return true; 418 419 default: 420 return false; 421 } 422 } 423 424 private void updateFilterTypeAndHeader(int filterType) { 425 mCallTypeFilter = filterType; 426 427 switch (filterType) { 428 case CallLogQueryHandler.CALL_TYPE_ALL: 429 mFilterStatusView.setVisibility(View.GONE); 430 break; 431 case Calls.INCOMING_TYPE: 432 showFilterStatus(R.string.call_log_incoming_header); 433 break; 434 case Calls.OUTGOING_TYPE: 435 showFilterStatus(R.string.call_log_outgoing_header); 436 break; 437 case Calls.MISSED_TYPE: 438 showFilterStatus(R.string.call_log_missed_header); 439 break; 440 case Calls.VOICEMAIL_TYPE: 441 showFilterStatus(R.string.call_log_voicemail_header); 442 break; 443 } 444 } 445 446 private void showFilterStatus(int resId) { 447 mFilterStatusView.setText(resId); 448 mFilterStatusView.setVisibility(View.VISIBLE); 449 } 450 451 public void callSelectedEntry() { 452 int position = getListView().getSelectedItemPosition(); 453 if (position < 0) { 454 // In touch mode you may often not have something selected, so 455 // just call the first entry to make sure that [send] [send] calls the 456 // most recent entry. 457 position = 0; 458 } 459 final Cursor cursor = (Cursor)mAdapter.getItem(position); 460 if (cursor != null) { 461 String number = cursor.getString(CallLogQuery.NUMBER); 462 int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); 463 if (!PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) { 464 // This number can't be called, do nothing 465 return; 466 } 467 Intent intent; 468 // If "number" is really a SIP address, construct a sip: URI. 469 if (PhoneNumberUtils.isUriNumber(number)) { 470 intent = CallUtil.getCallIntent( 471 Uri.fromParts(CallUtil.SCHEME_SIP, number, null)); 472 } else { 473 // We're calling a regular PSTN phone number. 474 // Construct a tel: URI, but do some other possible cleanup first. 475 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 476 if (!number.startsWith("+") && 477 (callType == Calls.INCOMING_TYPE 478 || callType == Calls.MISSED_TYPE)) { 479 // If the caller-id matches a contact with a better qualified number, use it 480 String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 481 number = mAdapter.getBetterNumberFromContacts(number, countryIso); 482 } 483 intent = CallUtil.getCallIntent( 484 Uri.fromParts(CallUtil.SCHEME_TEL, number, null)); 485 } 486 intent.setFlags( 487 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 488 startActivity(intent); 489 } 490 } 491 492 @VisibleForTesting 493 CallLogAdapter getAdapter() { 494 return mAdapter; 495 } 496 497 @Override 498 public void setMenuVisibility(boolean menuVisible) { 499 super.setMenuVisibility(menuVisible); 500 if (mMenuVisible != menuVisible) { 501 mMenuVisible = menuVisible; 502 if (!menuVisible) { 503 updateOnExit(); 504 } else if (isResumed()) { 505 refreshData(); 506 } 507 } 508 } 509 510 /** Requests updates to the data to be shown. */ 511 private void refreshData() { 512 // Prevent unnecessary refresh. 513 if (mRefreshDataRequired) { 514 // Mark all entries in the contact info cache as out of date, so they will be looked up 515 // again once being shown. 516 mAdapter.invalidateCache(); 517 startCallsQuery(); 518 startVoicemailStatusQuery(); 519 updateOnEntry(); 520 mRefreshDataRequired = false; 521 } 522 } 523 524 /** Removes the missed call notifications. */ 525 private void removeMissedCallNotifications() { 526 try { 527 ITelephony telephony = 528 ITelephony.Stub.asInterface(ServiceManager.getService("phone")); 529 if (telephony != null) { 530 telephony.cancelMissedCallsNotification(); 531 } else { 532 Log.w(TAG, "Telephony service is null, can't call " + 533 "cancelMissedCallsNotification"); 534 } 535 } catch (RemoteException e) { 536 Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); 537 } 538 } 539 540 /** Updates call data and notification state while leaving the call log tab. */ 541 private void updateOnExit() { 542 updateOnTransition(false); 543 } 544 545 /** Updates call data and notification state while entering the call log tab. */ 546 private void updateOnEntry() { 547 updateOnTransition(true); 548 } 549 550 private void updateOnTransition(boolean onEntry) { 551 // We don't want to update any call data when keyguard is on because the user has likely not 552 // seen the new calls yet. 553 // This might be called before onCreate() and thus we need to check null explicitly. 554 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 555 // On either of the transitions we reset the new flag and update the notifications. 556 // While exiting we additionally consume all missed calls (by marking them as read). 557 // This will ensure that they no more appear in the "new" section when we return back. 558 mCallLogQueryHandler.markNewCallsAsOld(); 559 if (!onEntry) { 560 mCallLogQueryHandler.markMissedCallsAsRead(); 561 } 562 removeMissedCallNotifications(); 563 updateVoicemailNotifications(); 564 } 565 } 566 567 private void updateVoicemailNotifications() { 568 Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class); 569 serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS); 570 getActivity().startService(serviceIntent); 571 } 572 573 /** 574 * Register a phone call filter to reset the call type when a phone call is place. 575 */ 576 private void registerPhoneCallReceiver() { 577 if (mPhoneStateListener != null) { 578 return; // Already registered. 579 } 580 mTelephonyManager = (TelephonyManager) getActivity().getSystemService( 581 Context.TELEPHONY_SERVICE); 582 mPhoneStateListener = new PhoneStateListener() { 583 @Override 584 public void onCallStateChanged(int state, String incomingNumber) { 585 if (state != TelephonyManager.CALL_STATE_OFFHOOK && 586 state != TelephonyManager.CALL_STATE_RINGING) { 587 return; 588 } 589 mHandler.post(new Runnable() { 590 @Override 591 public void run() { 592 if (getActivity() == null || getActivity().isFinishing()) { 593 return; 594 } 595 updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL); 596 } 597 }); 598 } 599 }; 600 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 601 } 602 603 /** 604 * Un-registers the phone call receiver. 605 */ 606 private void unregisterPhoneCallReceiver() { 607 if (mPhoneStateListener != null) { 608 mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); 609 mPhoneStateListener = null; 610 } 611 } 612} 613