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