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