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