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