CallLogFragment.java revision 58eaabcc31e23fd4c071ad911b96da6eea4abc28
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 View mEmptyListView; 96 private KeyguardManager mKeyguardManager; 97 98 private boolean mEmptyLoaderRunning; 99 private boolean mCallLogFetched; 100 private boolean mVoicemailStatusFetched; 101 102 private final Handler mHandler = new Handler(); 103 104 private class CustomContentObserver extends ContentObserver { 105 public CustomContentObserver() { 106 super(mHandler); 107 } 108 @Override 109 public void onChange(boolean selfChange) { 110 mRefreshDataRequired = true; 111 } 112 } 113 114 // See issue 6363009 115 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 116 private final ContentObserver mContactsObserver = new CustomContentObserver(); 117 private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); 118 private boolean mRefreshDataRequired = true; 119 120 // Exactly same variable is in Fragment as a package private. 121 private boolean mMenuVisible = true; 122 123 // Default to all calls. 124 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 125 126 // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} 127 // will be used. 128 private int mLogLimit = NO_LOG_LIMIT; 129 130 // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after 131 // the date filter are included. If zero, no date-based filtering occurs. 132 private long mDateLimit = NO_DATE_LIMIT; 133 134 public CallLogFragment() { 135 this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT); 136 } 137 138 public CallLogFragment(int filterType) { 139 this(filterType, NO_LOG_LIMIT); 140 } 141 142 public CallLogFragment(int filterType, int logLimit) { 143 super(); 144 mCallTypeFilter = filterType; 145 mLogLimit = logLimit; 146 } 147 148 /** 149 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 150 * after the specified date. 151 * @param filterType type of calls to include. 152 * @param dateLimit limits results to calls occurring on or after the specified date. 153 */ 154 public CallLogFragment(int filterType, long dateLimit) { 155 this(filterType, NO_LOG_LIMIT, dateLimit); 156 } 157 158 /** 159 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 160 * after the specified date. Also provides a means to limit the number of results returned. 161 * @param filterType type of calls to include. 162 * @param logLimit limits the number of results to return. 163 * @param dateLimit limits results to calls occurring on or after the specified date. 164 */ 165 public CallLogFragment(int filterType, int logLimit, long dateLimit) { 166 this(filterType, logLimit); 167 mDateLimit = dateLimit; 168 } 169 170 @Override 171 public void onCreate(Bundle state) { 172 super.onCreate(state); 173 if (state != null) { 174 mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); 175 mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); 176 mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); 177 } 178 179 final Activity activity = getActivity(); 180 final ContentResolver resolver = activity.getContentResolver(); 181 String currentCountryIso = GeoUtil.getCurrentCountryIso(activity); 182 mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit); 183 mKeyguardManager = 184 (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); 185 resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); 186 resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, 187 mContactsObserver); 188 resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver); 189 setHasOptionsMenu(true); 190 } 191 192 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 193 @Override 194 public boolean onCallsFetched(Cursor cursor) { 195 if (getActivity() == null || getActivity().isFinishing()) { 196 // Return false; we did not take ownership of the cursor 197 return false; 198 } 199 mAdapter.setLoading(false); 200 mAdapter.changeCursor(cursor); 201 // This will update the state of the "Clear call log" menu item. 202 getActivity().invalidateOptionsMenu(); 203 204 boolean showListView = cursor != null && cursor.getCount() > 0; 205 mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE); 206 mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); 207 208 if (mScrollToTop) { 209 // The smooth-scroll animation happens over a fixed time period. 210 // As a result, if it scrolls through a large portion of the list, 211 // each frame will jump so far from the previous one that the user 212 // will not experience the illusion of downward motion. Instead, 213 // if we're not already near the top of the list, we instantly jump 214 // near the top, and animate from there. 215 if (mLayoutManager.findFirstVisibleItemPosition() > 5) { 216 // TODO: Jump to near the top, then begin smooth scroll. 217 mRecyclerView.smoothScrollToPosition(0); 218 } 219 // Workaround for framework issue: the smooth-scroll doesn't 220 // occur if setSelection() is called immediately before. 221 mHandler.post(new Runnable() { 222 @Override 223 public void run() { 224 if (getActivity() == null || getActivity().isFinishing()) { 225 return; 226 } 227 mRecyclerView.smoothScrollToPosition(0); 228 } 229 }); 230 231 mScrollToTop = false; 232 } 233 mCallLogFetched = true; 234 destroyEmptyLoaderIfAllDataFetched(); 235 return true; 236 } 237 238 /** 239 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 240 */ 241 @Override 242 public void onVoicemailStatusFetched(Cursor statusCursor) { 243 Activity activity = getActivity(); 244 if (activity == null || activity.isFinishing()) { 245 return; 246 } 247 248 mVoicemailStatusFetched = true; 249 destroyEmptyLoaderIfAllDataFetched(); 250 } 251 252 private void destroyEmptyLoaderIfAllDataFetched() { 253 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 254 mEmptyLoaderRunning = false; 255 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 256 } 257 } 258 259 @Override 260 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 261 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 262 263 mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 264 mRecyclerView.setHasFixedSize(true); 265 mLayoutManager = new LinearLayoutManager(getActivity()); 266 mRecyclerView.setLayoutManager(mLayoutManager); 267 mEmptyListView = view.findViewById(R.id.empty_list_view); 268 269 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 270 boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT; 271 mAdapter = ObjectFactory.newCallLogAdapter( 272 getActivity(), 273 this, 274 new ContactInfoHelper(getActivity(), currentCountryIso), 275 isShowingRecentsTab, 276 this); 277 mRecyclerView.setAdapter(mAdapter); 278 279 fetchCalls(); 280 return view; 281 } 282 283 @Override 284 public void onViewCreated(View view, Bundle savedInstanceState) { 285 super.onViewCreated(view, savedInstanceState); 286 updateEmptyMessage(mCallTypeFilter); 287 } 288 289 /** 290 * Based on the new intent, decide whether the list should be configured 291 * to scroll up to display the first item. 292 */ 293 public void configureScreenFromIntent(Intent newIntent) { 294 // Typically, when switching to the call-log we want to show the user 295 // the same section of the list that they were most recently looking 296 // at. However, under some circumstances, we want to automatically 297 // scroll to the top of the list to present the newest call items. 298 // For example, immediately after a call is finished, we want to 299 // display information about that call. 300 mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType()); 301 } 302 303 @Override 304 public void onStart() { 305 // Start the empty loader now to defer other fragments. We destroy it when both calllog 306 // and the voicemail status are fetched. 307 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 308 new EmptyLoader.Callback(getActivity())); 309 mEmptyLoaderRunning = true; 310 super.onStart(); 311 } 312 313 @Override 314 public void onResume() { 315 super.onResume(); 316 refreshData(); 317 } 318 319 @Override 320 public void onPause() { 321 super.onPause(); 322 mAdapter.pauseCache(); 323 } 324 325 @Override 326 public void onStop() { 327 super.onStop(); 328 329 updateOnTransition(false /* onEntry */); 330 } 331 332 @Override 333 public void onDestroy() { 334 super.onDestroy(); 335 mAdapter.pauseCache(); 336 mAdapter.changeCursor(null); 337 getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); 338 getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); 339 getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); 340 } 341 342 @Override 343 public void onSaveInstanceState(Bundle outState) { 344 super.onSaveInstanceState(outState); 345 outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); 346 outState.putInt(KEY_LOG_LIMIT, mLogLimit); 347 outState.putLong(KEY_DATE_LIMIT, mDateLimit); 348 } 349 350 @Override 351 public void fetchCalls() { 352 mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); 353 } 354 355 private void updateEmptyMessage(int filterType) { 356 final int messageId; 357 switch (filterType) { 358 case Calls.MISSED_TYPE: 359 messageId = R.string.recentMissed_empty; 360 break; 361 case Calls.VOICEMAIL_TYPE: 362 messageId = R.string.recentVoicemails_empty; 363 break; 364 case CallLogQueryHandler.CALL_TYPE_ALL: 365 messageId = R.string.recentCalls_empty; 366 break; 367 default: 368 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " 369 + filterType); 370 } 371 DialerUtils.configureEmptyListView( 372 mEmptyListView, R.drawable.empty_call_log, messageId, getResources()); 373 } 374 375 CallLogAdapter getAdapter() { 376 return mAdapter; 377 } 378 379 @Override 380 public void setMenuVisibility(boolean menuVisible) { 381 super.setMenuVisibility(menuVisible); 382 if (mMenuVisible != menuVisible) { 383 mMenuVisible = menuVisible; 384 if (!menuVisible) { 385 updateOnTransition(false /* onEntry */); 386 } else if (isResumed()) { 387 refreshData(); 388 } 389 } 390 } 391 392 /** Requests updates to the data to be shown. */ 393 private void refreshData() { 394 // Prevent unnecessary refresh. 395 if (mRefreshDataRequired) { 396 // Mark all entries in the contact info cache as out of date, so they will be looked up 397 // again once being shown. 398 mAdapter.invalidateCache(); 399 mAdapter.setLoading(true); 400 401 fetchCalls(); 402 mCallLogQueryHandler.fetchVoicemailStatus(); 403 404 updateOnTransition(true /* onEntry */); 405 mRefreshDataRequired = false; 406 } else { 407 // Refresh the display of the existing data to update the timestamp text descriptions. 408 mAdapter.notifyDataSetChanged(); 409 } 410 } 411 412 /** 413 * Updates the call data and notification state on entering or leaving the call log tab. 414 * 415 * If we are leaving the call log tab, mark all the missed calls as read. 416 * 417 * TODO: Move to CallLogActivity 418 */ 419 private void updateOnTransition(boolean onEntry) { 420 // We don't want to update any call data when keyguard is on because the user has likely not 421 // seen the new calls yet. 422 // This might be called before onCreate() and thus we need to check null explicitly. 423 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 424 // On either of the transitions we update the missed call and voicemail notifications. 425 // While exiting we additionally consume all missed calls (by marking them as read). 426 mCallLogQueryHandler.markNewCallsAsOld(); 427 if (!onEntry) { 428 mCallLogQueryHandler.markMissedCallsAsRead(); 429 } 430 CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); 431 CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); 432 } 433 } 434 435 public void onBadDataReported(String number) { 436 if (number == null) { 437 return; 438 } 439 mAdapter.invalidateCache(); 440 mAdapter.notifyDataSetChanged(); 441 } 442 443 public void onReportButtonClick(String number) { 444 DialogFragment df = ObjectFactory.getReportDialogFragment(number); 445 if (df != null) { 446 df.setTargetFragment(this, 0); 447 df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG); 448 } 449 } 450} 451