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