CallLogFragment.java revision 2f02c8a8d840df38a09b55525a784dc666f622eb
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.telephony.TelephonyManager; 34import android.view.LayoutInflater; 35import android.view.View; 36import android.view.ViewGroup; 37import android.widget.ListView; 38import android.widget.TextView; 39 40import com.android.common.io.MoreCloseables; 41import com.android.contacts.common.CallUtil; 42import com.android.contacts.common.GeoUtil; 43import com.android.contacts.common.util.PhoneNumberHelper; 44import com.android.dialer.R; 45import com.android.dialer.util.EmptyLoader; 46import com.android.dialer.voicemail.VoicemailStatusHelper; 47import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 48import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 49import com.android.dialerbind.ObjectFactory; 50 51import java.util.List; 52 53/** 54 * Displays a list of call log entries. To filter for a particular kind of call 55 * (all, missed or voicemails), specify it in the constructor. 56 */ 57public class CallLogFragment extends ListFragment 58 implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { 59 private static final String TAG = "CallLogFragment"; 60 61 /** 62 * ID of the empty loader to defer other fragments. 63 */ 64 private static final int EMPTY_LOADER_ID = 0; 65 66 private CallLogAdapter mAdapter; 67 private CallLogQueryHandler mCallLogQueryHandler; 68 private boolean mScrollToTop; 69 70 /** Whether there is at least one voicemail source installed. */ 71 private boolean mVoicemailSourcesAvailable = false; 72 73 private VoicemailStatusHelper mVoicemailStatusHelper; 74 private View mStatusMessageView; 75 private TextView mStatusMessageText; 76 private TextView mStatusMessageAction; 77 private KeyguardManager mKeyguardManager; 78 79 private boolean mEmptyLoaderRunning; 80 private boolean mCallLogFetched; 81 private boolean mVoicemailStatusFetched; 82 83 private final Handler mHandler = new Handler(); 84 85 private TelephonyManager mTelephonyManager; 86 87 private class CustomContentObserver extends ContentObserver { 88 public CustomContentObserver() { 89 super(mHandler); 90 } 91 @Override 92 public void onChange(boolean selfChange) { 93 mRefreshDataRequired = true; 94 } 95 } 96 97 // See issue 6363009 98 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 99 private final ContentObserver mContactsObserver = new CustomContentObserver(); 100 private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); 101 private boolean mRefreshDataRequired = true; 102 103 // Exactly same variable is in Fragment as a package private. 104 private boolean mMenuVisible = true; 105 106 // Default to all calls. 107 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 108 109 // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} 110 // will be used. 111 private int mLogLimit = -1; 112 113 // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after 114 // the date filter are included. If zero, no date-based filtering occurs. 115 private long mDateLimit = 0; 116 117 public CallLogFragment() { 118 this(CallLogQueryHandler.CALL_TYPE_ALL, -1); 119 } 120 121 public CallLogFragment(int filterType) { 122 this(filterType, -1); 123 } 124 125 public CallLogFragment(int filterType, int logLimit) { 126 super(); 127 mCallTypeFilter = filterType; 128 mLogLimit = logLimit; 129 } 130 131 /** 132 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 133 * after the specified date. 134 * @param filterType type of calls to include. 135 * @param dateLimit limits results to calls occurring on or after the specified date. 136 */ 137 public CallLogFragment(int filterType, long dateLimit) { 138 this(filterType, -1, dateLimit); 139 } 140 141 /** 142 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 143 * after the specified date. Also provides a means to limit the number of results returned. 144 * @param filterType type of calls to include. 145 * @param logLimit limits the number of results to return. 146 * @param dateLimit limits results to calls occurring on or after the specified date. 147 */ 148 public CallLogFragment(int filterType, int logLimit, long dateLimit) { 149 this(filterType, logLimit); 150 mDateLimit = dateLimit; 151 } 152 153 @Override 154 public void onCreate(Bundle state) { 155 super.onCreate(state); 156 157 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), 158 this, mLogLimit); 159 mKeyguardManager = 160 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 161 getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true, 162 mCallLogObserver); 163 getActivity().getContentResolver().registerContentObserver( 164 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); 165 getActivity().getContentResolver().registerContentObserver( 166 Status.CONTENT_URI, true, mVoicemailStatusObserver); 167 setHasOptionsMenu(true); 168 updateCallList(mCallTypeFilter, mDateLimit); 169 } 170 171 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 172 @Override 173 public void onCallsFetched(Cursor cursor) { 174 if (getActivity() == null || getActivity().isFinishing()) { 175 return; 176 } 177 mAdapter.setLoading(false); 178 mAdapter.changeCursor(cursor); 179 // This will update the state of the "Clear call log" menu item. 180 getActivity().invalidateOptionsMenu(); 181 if (mScrollToTop) { 182 final ListView listView = getListView(); 183 // The smooth-scroll animation happens over a fixed time period. 184 // As a result, if it scrolls through a large portion of the list, 185 // each frame will jump so far from the previous one that the user 186 // will not experience the illusion of downward motion. Instead, 187 // if we're not already near the top of the list, we instantly jump 188 // near the top, and animate from there. 189 if (listView.getFirstVisiblePosition() > 5) { 190 listView.setSelection(5); 191 } 192 // Workaround for framework issue: the smooth-scroll doesn't 193 // occur if setSelection() is called immediately before. 194 mHandler.post(new Runnable() { 195 @Override 196 public void run() { 197 if (getActivity() == null || getActivity().isFinishing()) { 198 return; 199 } 200 listView.smoothScrollToPosition(0); 201 } 202 }); 203 204 mScrollToTop = false; 205 } 206 mCallLogFetched = true; 207 destroyEmptyLoaderIfAllDataFetched(); 208 } 209 210 /** 211 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 212 */ 213 @Override 214 public void onVoicemailStatusFetched(Cursor statusCursor) { 215 if (getActivity() == null || getActivity().isFinishing()) { 216 return; 217 } 218 updateVoicemailStatusMessage(statusCursor); 219 220 int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); 221 setVoicemailSourcesAvailable(activeSources != 0); 222 MoreCloseables.closeQuietly(statusCursor); 223 mVoicemailStatusFetched = true; 224 destroyEmptyLoaderIfAllDataFetched(); 225 } 226 227 private void destroyEmptyLoaderIfAllDataFetched() { 228 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 229 mEmptyLoaderRunning = false; 230 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 231 } 232 } 233 234 /** Sets whether there are any voicemail sources available in the platform. */ 235 private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { 236 if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; 237 mVoicemailSourcesAvailable = voicemailSourcesAvailable; 238 239 Activity activity = getActivity(); 240 if (activity != null) { 241 // This is so that the options menu content is updated. 242 activity.invalidateOptionsMenu(); 243 } 244 } 245 246 @Override 247 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 248 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 249 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 250 mStatusMessageView = view.findViewById(R.id.voicemail_status); 251 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 252 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 253 return view; 254 } 255 256 @Override 257 public void onViewCreated(View view, Bundle savedInstanceState) { 258 super.onViewCreated(view, savedInstanceState); 259 updateEmptyMessage(mCallTypeFilter); 260 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 261 mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper( 262 getActivity(), currentCountryIso), true, true); 263 setListAdapter(mAdapter); 264 getListView().setItemsCanFocus(true); 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 CallLogQueryHandler.CALL_TYPE_ALL: 374 message = getString(R.string.recentCalls_empty); 375 break; 376 default: 377 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " 378 + filterType); 379 } 380 ((TextView) getListView().getEmptyView()).setText(message); 381 } 382 383 public void callSelectedEntry() { 384 int position = getListView().getSelectedItemPosition(); 385 if (position < 0) { 386 // In touch mode you may often not have something selected, so 387 // just call the first entry to make sure that [send] [send] calls the 388 // most recent entry. 389 position = 0; 390 } 391 final Cursor cursor = (Cursor)mAdapter.getItem(position); 392 if (cursor != null) { 393 String number = cursor.getString(CallLogQuery.NUMBER); 394 int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); 395 if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { 396 // This number can't be called, do nothing 397 return; 398 } 399 Intent intent; 400 // If "number" is really a SIP address, construct a sip: URI. 401 if (PhoneNumberHelper.isUriNumber(number)) { 402 intent = CallUtil.getCallIntent( 403 Uri.fromParts(CallUtil.SCHEME_SIP, number, null)); 404 } else { 405 // We're calling a regular PSTN phone number. 406 // Construct a tel: URI, but do some other possible cleanup first. 407 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 408 if (!number.startsWith("+") && 409 (callType == Calls.INCOMING_TYPE 410 || callType == Calls.MISSED_TYPE)) { 411 // If the caller-id matches a contact with a better qualified number, use it 412 String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 413 number = mAdapter.getBetterNumberFromContacts(number, countryIso); 414 } 415 intent = CallUtil.getCallIntent( 416 Uri.fromParts(CallUtil.SCHEME_TEL, number, null)); 417 } 418 intent.setFlags( 419 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 420 startActivity(intent); 421 } 422 } 423 424 CallLogAdapter getAdapter() { 425 return mAdapter; 426 } 427 428 @Override 429 public void setMenuVisibility(boolean menuVisible) { 430 super.setMenuVisibility(menuVisible); 431 if (mMenuVisible != menuVisible) { 432 mMenuVisible = menuVisible; 433 if (!menuVisible) { 434 updateOnExit(); 435 } else if (isResumed()) { 436 refreshData(); 437 } 438 } 439 } 440 441 /** Requests updates to the data to be shown. */ 442 private void refreshData() { 443 // Prevent unnecessary refresh. 444 if (mRefreshDataRequired) { 445 // Mark all entries in the contact info cache as out of date, so they will be looked up 446 // again once being shown. 447 mAdapter.invalidateCache(); 448 startCallsQuery(); 449 startVoicemailStatusQuery(); 450 updateOnEntry(); 451 mRefreshDataRequired = false; 452 } 453 } 454 455 /** Updates call data and notification state while leaving the call log tab. */ 456 private void updateOnExit() { 457 updateOnTransition(false); 458 } 459 460 /** Updates call data and notification state while entering the call log tab. */ 461 private void updateOnEntry() { 462 updateOnTransition(true); 463 } 464 465 // TODO: Move to CallLogActivity 466 private void updateOnTransition(boolean onEntry) { 467 // We don't want to update any call data when keyguard is on because the user has likely not 468 // seen the new calls yet. 469 // This might be called before onCreate() and thus we need to check null explicitly. 470 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 471 // On either of the transitions we update the missed call and voicemail notifications. 472 // While exiting we additionally consume all missed calls (by marking them as read). 473 mCallLogQueryHandler.markNewCallsAsOld(); 474 if (!onEntry) { 475 mCallLogQueryHandler.markMissedCallsAsRead(); 476 } 477 CallLogNotificationsHelper.removeMissedCallNotifications(); 478 CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); 479 } 480 } 481} 482