CallLogFragment.java revision bb48628db6fd444460df61be7afa7fb633f47f50
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.ContentResolver; 27import android.content.Context; 28import android.content.Intent; 29import android.database.ContentObserver; 30import android.database.Cursor; 31import android.graphics.Rect; 32import android.os.Bundle; 33import android.os.Handler; 34import android.provider.CallLog; 35import android.provider.CallLog.Calls; 36import android.provider.ContactsContract; 37import android.provider.VoicemailContract.Status; 38import android.support.v7.widget.RecyclerView; 39import android.support.v7.widget.LinearLayoutManager; 40import android.view.LayoutInflater; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.View.OnClickListener; 44import android.view.ViewGroup.LayoutParams; 45import android.widget.ListView; 46import android.widget.TextView; 47 48import com.android.contacts.common.GeoUtil; 49import com.android.contacts.common.util.ViewUtil; 50import com.android.dialer.R; 51import com.android.dialer.list.ListsFragment.HostInterface; 52import com.android.dialer.util.DialerUtils; 53import com.android.dialer.util.EmptyLoader; 54import com.android.dialer.voicemail.VoicemailStatusHelper; 55import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 56import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 57import com.android.dialerbind.ObjectFactory; 58 59import java.util.List; 60 61/** 62 * Displays a list of call log entries. To filter for a particular kind of call 63 * (all, missed or voicemails), specify it in the constructor. 64 */ 65public class CallLogFragment extends Fragment 66 implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener, 67 CallLogAdapter.CallFetcher { 68 private static final String TAG = "CallLogFragment"; 69 70 private static final String REPORT_DIALOG_TAG = "report_dialog"; 71 72 /** 73 * ID of the empty loader to defer other fragments. 74 */ 75 private static final int EMPTY_LOADER_ID = 0; 76 77 private static final String KEY_FILTER_TYPE = "filter_type"; 78 private static final String KEY_LOG_LIMIT = "log_limit"; 79 private static final String KEY_DATE_LIMIT = "date_limit"; 80 81 // No limit specified for the number of logs to show; use the CallLogQueryHandler's default. 82 private static final int NO_LOG_LIMIT = -1; 83 // No date-based filtering. 84 private static final int NO_DATE_LIMIT = 0; 85 86 private RecyclerView mRecyclerView; 87 private LinearLayoutManager mLayoutManager; 88 private CallLogAdapter mAdapter; 89 private CallLogQueryHandler mCallLogQueryHandler; 90 private boolean mScrollToTop; 91 92 /** Whether there is at least one voicemail source installed. */ 93 private boolean mVoicemailSourcesAvailable = false; 94 95 private VoicemailStatusHelper mVoicemailStatusHelper; 96 private View mStatusMessageView; 97 private View mEmptyListView; 98 private TextView mStatusMessageText; 99 private TextView mStatusMessageAction; 100 private KeyguardManager mKeyguardManager; 101 102 private boolean mEmptyLoaderRunning; 103 private boolean mCallLogFetched; 104 private boolean mVoicemailStatusFetched; 105 106 private final Handler mHandler = new Handler(); 107 108 private class CustomContentObserver extends ContentObserver { 109 public CustomContentObserver() { 110 super(mHandler); 111 } 112 @Override 113 public void onChange(boolean selfChange) { 114 mRefreshDataRequired = true; 115 } 116 } 117 118 // See issue 6363009 119 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 120 private final ContentObserver mContactsObserver = new CustomContentObserver(); 121 private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); 122 private boolean mRefreshDataRequired = true; 123 124 // Exactly same variable is in Fragment as a package private. 125 private boolean mMenuVisible = true; 126 127 // Default to all calls. 128 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 129 130 // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} 131 // will be used. 132 private int mLogLimit = NO_LOG_LIMIT; 133 134 // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after 135 // the date filter are included. If zero, no date-based filtering occurs. 136 private long mDateLimit = NO_DATE_LIMIT; 137 138 public CallLogFragment() { 139 this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT); 140 } 141 142 public CallLogFragment(int filterType) { 143 this(filterType, NO_LOG_LIMIT); 144 } 145 146 public CallLogFragment(int filterType, int logLimit) { 147 super(); 148 mCallTypeFilter = filterType; 149 mLogLimit = logLimit; 150 } 151 152 /** 153 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 154 * after the specified date. 155 * @param filterType type of calls to include. 156 * @param dateLimit limits results to calls occurring on or after the specified date. 157 */ 158 public CallLogFragment(int filterType, long dateLimit) { 159 this(filterType, NO_LOG_LIMIT, dateLimit); 160 } 161 162 /** 163 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 164 * after the specified date. Also provides a means to limit the number of results returned. 165 * @param filterType type of calls to include. 166 * @param logLimit limits the number of results to return. 167 * @param dateLimit limits results to calls occurring on or after the specified date. 168 */ 169 public CallLogFragment(int filterType, int logLimit, long dateLimit) { 170 this(filterType, logLimit); 171 mDateLimit = dateLimit; 172 } 173 174 @Override 175 public void onCreate(Bundle state) { 176 super.onCreate(state); 177 if (state != null) { 178 mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); 179 mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); 180 mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); 181 } 182 183 final Activity activity = getActivity(); 184 final ContentResolver resolver = activity.getContentResolver(); 185 String currentCountryIso = GeoUtil.getCurrentCountryIso(activity); 186 mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit); 187 mKeyguardManager = 188 (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); 189 resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); 190 resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, 191 mContactsObserver); 192 resolver.registerContentObserver(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 boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT; 285 mAdapter = ObjectFactory.newCallLogAdapter( 286 getActivity(), 287 this, 288 new ContactInfoHelper(getActivity(), currentCountryIso), 289 isShowingRecentsTab, 290 this); 291 mRecyclerView.setAdapter(mAdapter); 292 293 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 294 mStatusMessageView = view.findViewById(R.id.voicemail_status); 295 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 296 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 297 return view; 298 } 299 300 @Override 301 public void onViewCreated(View view, Bundle savedInstanceState) { 302 super.onViewCreated(view, savedInstanceState); 303 mEmptyListView = view.findViewById(R.id.empty_list_view); 304 305 updateEmptyMessage(mCallTypeFilter); 306 } 307 308 /** 309 * Based on the new intent, decide whether the list should be configured 310 * to scroll up to display the first item. 311 */ 312 public void configureScreenFromIntent(Intent newIntent) { 313 // Typically, when switching to the call-log we want to show the user 314 // the same section of the list that they were most recently looking 315 // at. However, under some circumstances, we want to automatically 316 // scroll to the top of the list to present the newest call items. 317 // For example, immediately after a call is finished, we want to 318 // display information about that call. 319 mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType()); 320 } 321 322 @Override 323 public void onStart() { 324 // Start the empty loader now to defer other fragments. We destroy it when both calllog 325 // and the voicemail status are fetched. 326 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 327 new EmptyLoader.Callback(getActivity())); 328 mEmptyLoaderRunning = true; 329 super.onStart(); 330 } 331 332 @Override 333 public void onResume() { 334 super.onResume(); 335 refreshData(); 336 } 337 338 private void updateVoicemailStatusMessage(Cursor statusCursor) { 339 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 340 if (messages.size() == 0) { 341 mStatusMessageView.setVisibility(View.GONE); 342 } else { 343 mStatusMessageView.setVisibility(View.VISIBLE); 344 // TODO: Change the code to show all messages. For now just pick the first message. 345 final StatusMessage message = messages.get(0); 346 if (message.showInCallLog()) { 347 mStatusMessageText.setText(message.callLogMessageId); 348 } 349 if (message.actionMessageId != -1) { 350 mStatusMessageAction.setText(message.actionMessageId); 351 } 352 if (message.actionUri != null) { 353 mStatusMessageAction.setVisibility(View.VISIBLE); 354 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 355 @Override 356 public void onClick(View v) { 357 getActivity().startActivity( 358 new Intent(Intent.ACTION_VIEW, message.actionUri)); 359 } 360 }); 361 } else { 362 mStatusMessageAction.setVisibility(View.GONE); 363 } 364 } 365 } 366 367 @Override 368 public void onPause() { 369 super.onPause(); 370 mAdapter.pauseCache(); 371 } 372 373 @Override 374 public void onStop() { 375 super.onStop(); 376 377 updateOnTransition(false /* onEntry */); 378 } 379 380 @Override 381 public void onDestroy() { 382 super.onDestroy(); 383 mAdapter.pauseCache(); 384 mAdapter.changeCursor(null); 385 getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); 386 getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); 387 getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); 388 } 389 390 @Override 391 public void onSaveInstanceState(Bundle outState) { 392 super.onSaveInstanceState(outState); 393 outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); 394 outState.putInt(KEY_LOG_LIMIT, mLogLimit); 395 outState.putLong(KEY_DATE_LIMIT, mDateLimit); 396 } 397 398 @Override 399 public void fetchCalls() { 400 mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); 401 } 402 403 private void updateEmptyMessage(int filterType) { 404 final int messageId; 405 switch (filterType) { 406 case Calls.MISSED_TYPE: 407 messageId = R.string.recentMissed_empty; 408 break; 409 case Calls.VOICEMAIL_TYPE: 410 messageId = R.string.recentVoicemails_empty; 411 break; 412 case CallLogQueryHandler.CALL_TYPE_ALL: 413 messageId = R.string.recentCalls_empty; 414 break; 415 default: 416 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " 417 + filterType); 418 } 419 DialerUtils.configureEmptyListView( 420 mEmptyListView, R.drawable.empty_call_log, messageId, getResources()); 421 } 422 423 CallLogAdapter getAdapter() { 424 return mAdapter; 425 } 426 427 @Override 428 public void setMenuVisibility(boolean menuVisible) { 429 super.setMenuVisibility(menuVisible); 430 if (mMenuVisible != menuVisible) { 431 mMenuVisible = menuVisible; 432 if (!menuVisible) { 433 updateOnTransition(false /* onEntry */); 434 } else if (isResumed()) { 435 refreshData(); 436 } 437 } 438 } 439 440 /** Requests updates to the data to be shown. */ 441 private void refreshData() { 442 // Prevent unnecessary refresh. 443 if (mRefreshDataRequired) { 444 // Mark all entries in the contact info cache as out of date, so they will be looked up 445 // again once being shown. 446 mAdapter.invalidateCache(); 447 mAdapter.setLoading(true); 448 449 fetchCalls(); 450 mCallLogQueryHandler.fetchVoicemailStatus(); 451 452 updateOnTransition(true /* onEntry */); 453 mRefreshDataRequired = false; 454 } else { 455 // Refresh the display of the existing data to update the timestamp text descriptions. 456 mAdapter.notifyDataSetChanged(); 457 } 458 } 459 460 /** 461 * Updates the call data and notification state on entering or leaving the call log tab. 462 * 463 * If we are leaving the call log tab, mark all the missed calls as read. 464 * 465 * TODO: Move to CallLogActivity 466 */ 467 private void updateOnTransition(boolean onEntry) { 468 // We don't want to update any call data when keyguard is on because the user has likely not 469 // seen the new calls yet. 470 // This might be called before onCreate() and thus we need to check null explicitly. 471 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 472 // On either of the transitions we update the missed call and voicemail notifications. 473 // While exiting we additionally consume all missed calls (by marking them as read). 474 mCallLogQueryHandler.markNewCallsAsOld(); 475 if (!onEntry) { 476 mCallLogQueryHandler.markMissedCallsAsRead(); 477 } 478 CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); 479 CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); 480 } 481 } 482 483 public void onBadDataReported(String number) { 484 if (number == null) { 485 return; 486 } 487 mAdapter.invalidateCache(); 488 mAdapter.notifyDataSetChanged(); 489 } 490 491 public void onReportButtonClick(String number) { 492 DialogFragment df = ObjectFactory.getReportDialogFragment(number); 493 if (df != null) { 494 df.setTargetFragment(this, 0); 495 df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG); 496 } 497 } 498} 499