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