CallLogFragment.java revision d0cabd3a4ea4ae4dd7baa7e2e28610b1d969fb02
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.text.TextUtils; 36import android.util.Log; 37import android.view.LayoutInflater; 38import android.view.Menu; 39import android.view.MenuInflater; 40import android.view.MenuItem; 41import android.view.View; 42import android.view.ViewGroup; 43import android.widget.ListView; 44import android.widget.TextView; 45 46import com.android.common.io.MoreCloseables; 47import com.android.contacts.common.CallUtil; 48import com.android.contacts.ContactsUtils; 49import com.android.contacts.R; 50import com.android.dialer.util.EmptyLoader; 51import com.android.dialer.voicemail.VoicemailStatusHelper; 52import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 53import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 54import com.android.internal.telephony.CallerInfo; 55import com.android.internal.telephony.ITelephony; 56import com.google.common.annotations.VisibleForTesting; 57 58import java.util.List; 59 60/** 61 * Displays a list of call log entries. 62 */ 63public class CallLogFragment extends ListFragment 64 implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { 65 private static final String TAG = "CallLogFragment"; 66 67 /** 68 * ID of the empty loader to defer other fragments. 69 */ 70 private static final int EMPTY_LOADER_ID = 0; 71 72 private CallLogAdapter mAdapter; 73 private CallLogQueryHandler mCallLogQueryHandler; 74 private boolean mScrollToTop; 75 76 /** Whether there is at least one voicemail source installed. */ 77 private boolean mVoicemailSourcesAvailable = false; 78 /** Whether we are currently filtering over voicemail. */ 79 private boolean mShowingVoicemailOnly = 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 class CustomContentObserver extends ContentObserver { 95 public CustomContentObserver() { 96 super(mHandler); 97 } 98 @Override 99 public void onChange(boolean selfChange) { 100 mRefreshDataRequired = true; 101 } 102 } 103 104 // See issue 6363009 105 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 106 private final ContentObserver mContactsObserver = new CustomContentObserver(); 107 private boolean mRefreshDataRequired = true; 108 109 // Exactly same variable is in Fragment as a package private. 110 private boolean mMenuVisible = true; 111 112 // Default to all calls. 113 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 114 115 @Override 116 public void onCreate(Bundle state) { 117 super.onCreate(state); 118 119 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); 120 mKeyguardManager = 121 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 122 getActivity().getContentResolver().registerContentObserver( 123 CallLog.CONTENT_URI, true, mCallLogObserver); 124 getActivity().getContentResolver().registerContentObserver( 125 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); 126 setHasOptionsMenu(true); 127 } 128 129 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 130 @Override 131 public void onCallsFetched(Cursor cursor) { 132 if (getActivity() == null || getActivity().isFinishing()) { 133 return; 134 } 135 mAdapter.setLoading(false); 136 mAdapter.changeCursor(cursor); 137 // This will update the state of the "Clear call log" menu item. 138 getActivity().invalidateOptionsMenu(); 139 if (mScrollToTop) { 140 final ListView listView = getListView(); 141 // The smooth-scroll animation happens over a fixed time period. 142 // As a result, if it scrolls through a large portion of the list, 143 // each frame will jump so far from the previous one that the user 144 // will not experience the illusion of downward motion. Instead, 145 // if we're not already near the top of the list, we instantly jump 146 // near the top, and animate from there. 147 if (listView.getFirstVisiblePosition() > 5) { 148 listView.setSelection(5); 149 } 150 // Workaround for framework issue: the smooth-scroll doesn't 151 // occur if setSelection() is called immediately before. 152 mHandler.post(new Runnable() { 153 @Override 154 public void run() { 155 if (getActivity() == null || getActivity().isFinishing()) { 156 return; 157 } 158 listView.smoothScrollToPosition(0); 159 } 160 }); 161 162 mScrollToTop = false; 163 } 164 mCallLogFetched = true; 165 destroyEmptyLoaderIfAllDataFetched(); 166 } 167 168 /** 169 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 170 */ 171 @Override 172 public void onVoicemailStatusFetched(Cursor statusCursor) { 173 if (getActivity() == null || getActivity().isFinishing()) { 174 return; 175 } 176 updateVoicemailStatusMessage(statusCursor); 177 178 int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); 179 setVoicemailSourcesAvailable(activeSources != 0); 180 MoreCloseables.closeQuietly(statusCursor); 181 mVoicemailStatusFetched = true; 182 destroyEmptyLoaderIfAllDataFetched(); 183 } 184 185 private void destroyEmptyLoaderIfAllDataFetched() { 186 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 187 mEmptyLoaderRunning = false; 188 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 189 } 190 } 191 192 /** Sets whether there are any voicemail sources available in the platform. */ 193 private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { 194 if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; 195 mVoicemailSourcesAvailable = voicemailSourcesAvailable; 196 197 Activity activity = getActivity(); 198 if (activity != null) { 199 // This is so that the options menu content is updated. 200 activity.invalidateOptionsMenu(); 201 } 202 } 203 204 @Override 205 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 206 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 207 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 208 mStatusMessageView = view.findViewById(R.id.voicemail_status); 209 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 210 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 211 mFilterStatusView = (TextView) view.findViewById(R.id.filter_status); 212 return view; 213 } 214 215 @Override 216 public void onViewCreated(View view, Bundle savedInstanceState) { 217 super.onViewCreated(view, savedInstanceState); 218 String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); 219 mAdapter = new CallLogAdapter(getActivity(), this, 220 new ContactInfoHelper(getActivity(), currentCountryIso)); 221 setListAdapter(mAdapter); 222 getListView().setItemsCanFocus(true); 223 224 updateFilterHeader(); 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 } 307 308 @Override 309 public void fetchCalls() { 310 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 311 } 312 313 public void startCallsQuery() { 314 mAdapter.setLoading(true); 315 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 316 if (mShowingVoicemailOnly) { 317 mShowingVoicemailOnly = false; 318 getActivity().invalidateOptionsMenu(); 319 } 320 } 321 322 private void startVoicemailStatusQuery() { 323 mCallLogQueryHandler.fetchVoicemailStatus(); 324 } 325 326 @Override 327 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 328 super.onCreateOptionsMenu(menu, inflater); 329 inflater.inflate(R.menu.call_log_options, menu); 330 } 331 332 @Override 333 public void onPrepareOptionsMenu(Menu menu) { 334 final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); 335 // Check if all the menu items are inflated correctly. As a shortcut, we assume all 336 // menu items are ready if the first item is non-null. 337 if (itemDeleteAll != null) { 338 itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty()); 339 menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable); 340 } 341 } 342 343 @Override 344 public boolean onOptionsItemSelected(MenuItem item) { 345 switch (item.getItemId()) { 346 case R.id.delete_all: 347 ClearCallLogDialog.show(getFragmentManager()); 348 return true; 349 350 case R.id.show_outgoing_only: 351 mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE); 352 mCallTypeFilter = Calls.OUTGOING_TYPE; 353 updateFilterHeader(); 354 return true; 355 356 case R.id.show_incoming_only: 357 mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE); 358 mCallTypeFilter = Calls.INCOMING_TYPE; 359 updateFilterHeader(); 360 return true; 361 362 case R.id.show_missed_only: 363 mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE); 364 mCallTypeFilter = Calls.MISSED_TYPE; 365 updateFilterHeader(); 366 return true; 367 368 case R.id.show_voicemails_only: 369 mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE); 370 mCallTypeFilter = Calls.VOICEMAIL_TYPE; 371 updateFilterHeader(); 372 mShowingVoicemailOnly = true; 373 return true; 374 375 case R.id.show_all_calls: 376 mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL); 377 mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 378 updateFilterHeader(); 379 mShowingVoicemailOnly = false; 380 return true; 381 382 default: 383 return false; 384 } 385 } 386 387 private void updateFilterHeader() { 388 switch (mCallTypeFilter) { 389 case CallLogQueryHandler.CALL_TYPE_ALL: 390 mFilterStatusView.setVisibility(View.GONE); 391 break; 392 case Calls.INCOMING_TYPE: 393 showFilterStatus(R.string.call_log_incoming_header); 394 break; 395 case Calls.OUTGOING_TYPE: 396 showFilterStatus(R.string.call_log_outgoing_header); 397 break; 398 case Calls.MISSED_TYPE: 399 showFilterStatus(R.string.call_log_missed_header); 400 break; 401 case Calls.VOICEMAIL_TYPE: 402 showFilterStatus(R.string.call_log_voicemail_header); 403 break; 404 } 405 } 406 407 private void showFilterStatus(int resId) { 408 mFilterStatusView.setText(resId); 409 mFilterStatusView.setVisibility(View.VISIBLE); 410 } 411 412 public void callSelectedEntry() { 413 int position = getListView().getSelectedItemPosition(); 414 if (position < 0) { 415 // In touch mode you may often not have something selected, so 416 // just call the first entry to make sure that [send] [send] calls the 417 // most recent entry. 418 position = 0; 419 } 420 final Cursor cursor = (Cursor)mAdapter.getItem(position); 421 if (cursor != null) { 422 String number = cursor.getString(CallLogQuery.NUMBER); 423 if (TextUtils.isEmpty(number) 424 || number.equals(CallerInfo.UNKNOWN_NUMBER) 425 || number.equals(CallerInfo.PRIVATE_NUMBER) 426 || number.equals(CallerInfo.PAYPHONE_NUMBER)) { 427 // This number can't be called, do nothing 428 return; 429 } 430 Intent intent; 431 // If "number" is really a SIP address, construct a sip: URI. 432 if (PhoneNumberUtils.isUriNumber(number)) { 433 intent = CallUtil.getCallIntent( 434 Uri.fromParts(CallUtil.SCHEME_SIP, number, null)); 435 } else { 436 // We're calling a regular PSTN phone number. 437 // Construct a tel: URI, but do some other possible cleanup first. 438 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 439 if (!number.startsWith("+") && 440 (callType == Calls.INCOMING_TYPE 441 || callType == Calls.MISSED_TYPE)) { 442 // If the caller-id matches a contact with a better qualified number, use it 443 String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 444 number = mAdapter.getBetterNumberFromContacts(number, countryIso); 445 } 446 intent = CallUtil.getCallIntent( 447 Uri.fromParts(CallUtil.SCHEME_TEL, number, null)); 448 } 449 intent.setFlags( 450 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 451 startActivity(intent); 452 } 453 } 454 455 @VisibleForTesting 456 CallLogAdapter getAdapter() { 457 return mAdapter; 458 } 459 460 @Override 461 public void setMenuVisibility(boolean menuVisible) { 462 super.setMenuVisibility(menuVisible); 463 if (mMenuVisible != menuVisible) { 464 mMenuVisible = menuVisible; 465 if (!menuVisible) { 466 updateOnExit(); 467 } else if (isResumed()) { 468 refreshData(); 469 } 470 } 471 } 472 473 /** Requests updates to the data to be shown. */ 474 private void refreshData() { 475 // Prevent unnecessary refresh. 476 if (mRefreshDataRequired) { 477 // Mark all entries in the contact info cache as out of date, so they will be looked up 478 // again once being shown. 479 mAdapter.invalidateCache(); 480 startCallsQuery(); 481 startVoicemailStatusQuery(); 482 updateOnEntry(); 483 mRefreshDataRequired = false; 484 } 485 } 486 487 /** Removes the missed call notifications. */ 488 private void removeMissedCallNotifications() { 489 try { 490 ITelephony telephony = 491 ITelephony.Stub.asInterface(ServiceManager.getService("phone")); 492 if (telephony != null) { 493 telephony.cancelMissedCallsNotification(); 494 } else { 495 Log.w(TAG, "Telephony service is null, can't call " + 496 "cancelMissedCallsNotification"); 497 } 498 } catch (RemoteException e) { 499 Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); 500 } 501 } 502 503 /** Updates call data and notification state while leaving the call log tab. */ 504 private void updateOnExit() { 505 updateOnTransition(false); 506 } 507 508 /** Updates call data and notification state while entering the call log tab. */ 509 private void updateOnEntry() { 510 updateOnTransition(true); 511 } 512 513 private void updateOnTransition(boolean onEntry) { 514 // We don't want to update any call data when keyguard is on because the user has likely not 515 // seen the new calls yet. 516 // This might be called before onCreate() and thus we need to check null explicitly. 517 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 518 // On either of the transitions we reset the new flag and update the notifications. 519 // While exiting we additionally consume all missed calls (by marking them as read). 520 // This will ensure that they no more appear in the "new" section when we return back. 521 mCallLogQueryHandler.markNewCallsAsOld(); 522 if (!onEntry) { 523 mCallLogQueryHandler.markMissedCallsAsRead(); 524 } 525 removeMissedCallNotifications(); 526 updateVoicemailNotifications(); 527 } 528 } 529 530 private void updateVoicemailNotifications() { 531 Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class); 532 serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS); 533 getActivity().startService(serviceIntent); 534 } 535} 536