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