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 is the Recents screen shown in 145 * DialtactsActivity. 146 */ 147 private boolean mIsRecentsFragment; 148 149 public interface HostInterface { 150 public void showDialpad(); 151 } 152 153 public CallLogFragment() { 154 this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT); 155 } 156 157 public CallLogFragment(int filterType) { 158 this(filterType, NO_LOG_LIMIT); 159 } 160 161 public CallLogFragment(int filterType, int logLimit) { 162 this(filterType, logLimit, NO_DATE_LIMIT); 163 } 164 165 /** 166 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 167 * after the specified date. 168 * @param filterType type of calls to include. 169 * @param dateLimit limits results to calls occurring on or after the specified date. 170 */ 171 public CallLogFragment(int filterType, long dateLimit) { 172 this(filterType, NO_LOG_LIMIT, dateLimit); 173 } 174 175 /** 176 * Creates a call log fragment, filtering to include only calls of the desired type, occurring 177 * after the specified date. Also provides a means to limit the number of results returned. 178 * @param filterType type of calls to include. 179 * @param logLimit limits the number of results to return. 180 * @param dateLimit limits results to calls occurring on or after the specified date. 181 */ 182 public CallLogFragment(int filterType, int logLimit, long dateLimit) { 183 mCallTypeFilter = filterType; 184 mLogLimit = logLimit; 185 mDateLimit = dateLimit; 186 } 187 188 @Override 189 public void onCreate(Bundle state) { 190 super.onCreate(state); 191 if (state != null) { 192 mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); 193 mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit); 194 mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit); 195 } 196 197 mIsRecentsFragment = mLogLimit != NO_LOG_LIMIT; 198 199 final Activity activity = getActivity(); 200 final ContentResolver resolver = activity.getContentResolver(); 201 String currentCountryIso = GeoUtil.getCurrentCountryIso(activity); 202 mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit); 203 mKeyguardManager = 204 (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); 205 resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); 206 resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, 207 mContactsObserver); 208 resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver); 209 setHasOptionsMenu(true); 210 211 if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) { 212 mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter 213 .getInstance(activity, state); 214 } 215 } 216 217 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 218 @Override 219 public boolean onCallsFetched(Cursor cursor) { 220 if (getActivity() == null || getActivity().isFinishing()) { 221 // Return false; we did not take ownership of the cursor 222 return false; 223 } 224 mAdapter.setLoading(false); 225 mAdapter.changeCursor(cursor); 226 // This will update the state of the "Clear call log" menu item. 227 getActivity().invalidateOptionsMenu(); 228 229 boolean showListView = cursor != null && cursor.getCount() > 0; 230 mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE); 231 mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); 232 233 if (mScrollToTop) { 234 // The smooth-scroll animation happens over a fixed time period. 235 // As a result, if it scrolls through a large portion of the list, 236 // each frame will jump so far from the previous one that the user 237 // will not experience the illusion of downward motion. Instead, 238 // if we're not already near the top of the list, we instantly jump 239 // near the top, and animate from there. 240 if (mLayoutManager.findFirstVisibleItemPosition() > 5) { 241 // TODO: Jump to near the top, then begin smooth scroll. 242 mRecyclerView.smoothScrollToPosition(0); 243 } 244 // Workaround for framework issue: the smooth-scroll doesn't 245 // occur if setSelection() is called immediately before. 246 mHandler.post(new Runnable() { 247 @Override 248 public void run() { 249 if (getActivity() == null || getActivity().isFinishing()) { 250 return; 251 } 252 mRecyclerView.smoothScrollToPosition(0); 253 } 254 }); 255 256 mScrollToTop = false; 257 } 258 mCallLogFetched = true; 259 destroyEmptyLoaderIfAllDataFetched(); 260 return true; 261 } 262 263 /** 264 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 265 */ 266 @Override 267 public void onVoicemailStatusFetched(Cursor statusCursor) { 268 Activity activity = getActivity(); 269 if (activity == null || activity.isFinishing()) { 270 return; 271 } 272 273 mVoicemailStatusFetched = true; 274 destroyEmptyLoaderIfAllDataFetched(); 275 } 276 277 private void destroyEmptyLoaderIfAllDataFetched() { 278 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 279 mEmptyLoaderRunning = false; 280 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 281 } 282 } 283 284 @Override 285 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 286 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 287 288 mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 289 mRecyclerView.setHasFixedSize(true); 290 mLayoutManager = new LinearLayoutManager(getActivity()); 291 mRecyclerView.setLayoutManager(mLayoutManager); 292 mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); 293 mEmptyListView.setImage(R.drawable.empty_call_log); 294 mEmptyListView.setActionClickedListener(this); 295 296 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 297 boolean isShowingRecentsTab = mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT; 298 mAdapter = ObjectFactory.newCallLogAdapter( 299 getActivity(), 300 this, 301 new ContactInfoHelper(getActivity(), currentCountryIso), 302 mVoicemailPlaybackPresenter, 303 isShowingRecentsTab); 304 mRecyclerView.setAdapter(mAdapter); 305 306 fetchCalls(); 307 return view; 308 } 309 310 @Override 311 public void onViewCreated(View view, Bundle savedInstanceState) { 312 super.onViewCreated(view, savedInstanceState); 313 updateEmptyMessage(mCallTypeFilter); 314 mAdapter.onRestoreInstanceState(savedInstanceState); 315 } 316 317 @Override 318 public void onStart() { 319 // Start the empty loader now to defer other fragments. We destroy it when both calllog 320 // and the voicemail status are fetched. 321 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 322 new EmptyLoader.Callback(getActivity())); 323 mEmptyLoaderRunning = true; 324 super.onStart(); 325 } 326 327 @Override 328 public void onResume() { 329 super.onResume(); 330 final boolean hasReadCallLogPermission = 331 PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); 332 if (!mHasReadCallLogPermission && hasReadCallLogPermission) { 333 // We didn't have the permission before, and now we do. Force a refresh of the call log. 334 // Note that this code path always happens on a fresh start, but mRefreshDataRequired 335 // is already true in that case anyway. 336 mRefreshDataRequired = true; 337 updateEmptyMessage(mCallTypeFilter); 338 } 339 mHasReadCallLogPermission = hasReadCallLogPermission; 340 refreshData(); 341 mAdapter.startCache(); 342 } 343 344 @Override 345 public void onPause() { 346 if (mVoicemailPlaybackPresenter != null) { 347 mVoicemailPlaybackPresenter.onPause(); 348 } 349 mAdapter.pauseCache(); 350 super.onPause(); 351 } 352 353 @Override 354 public void onStop() { 355 updateOnTransition(false /* onEntry */); 356 357 super.onStop(); 358 } 359 360 @Override 361 public void onDestroy() { 362 mAdapter.pauseCache(); 363 mAdapter.changeCursor(null); 364 365 if (mVoicemailPlaybackPresenter != null) { 366 mVoicemailPlaybackPresenter.onDestroy(); 367 } 368 369 getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); 370 getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); 371 getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); 372 super.onDestroy(); 373 } 374 375 @Override 376 public void onSaveInstanceState(Bundle outState) { 377 super.onSaveInstanceState(outState); 378 outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); 379 outState.putInt(KEY_LOG_LIMIT, mLogLimit); 380 outState.putLong(KEY_DATE_LIMIT, mDateLimit); 381 382 mAdapter.onSaveInstanceState(outState); 383 384 if (mVoicemailPlaybackPresenter != null) { 385 mVoicemailPlaybackPresenter.onSaveInstanceState(outState); 386 } 387 } 388 389 @Override 390 public void fetchCalls() { 391 mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit); 392 } 393 394 private void updateEmptyMessage(int filterType) { 395 final Context context = getActivity(); 396 if (context == null) { 397 return; 398 } 399 400 if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { 401 mEmptyListView.setDescription(R.string.permission_no_calllog); 402 mEmptyListView.setActionLabel(R.string.permission_single_turn_on); 403 return; 404 } 405 406 final int messageId; 407 switch (filterType) { 408 case Calls.MISSED_TYPE: 409 messageId = R.string.recentMissed_empty; 410 break; 411 case Calls.VOICEMAIL_TYPE: 412 messageId = R.string.recentVoicemails_empty; 413 break; 414 case CallLogQueryHandler.CALL_TYPE_ALL: 415 messageId = R.string.recentCalls_empty; 416 break; 417 default: 418 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " 419 + filterType); 420 } 421 mEmptyListView.setDescription(messageId); 422 if (mIsRecentsFragment) { 423 mEmptyListView.setActionLabel(R.string.recentCalls_empty_action); 424 } else { 425 mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); 426 } 427 } 428 429 CallLogAdapter getAdapter() { 430 return mAdapter; 431 } 432 433 @Override 434 public void setMenuVisibility(boolean menuVisible) { 435 super.setMenuVisibility(menuVisible); 436 if (mMenuVisible != menuVisible) { 437 mMenuVisible = menuVisible; 438 if (!menuVisible) { 439 updateOnTransition(false /* onEntry */); 440 } else if (isResumed()) { 441 refreshData(); 442 } 443 } 444 } 445 446 /** Requests updates to the data to be shown. */ 447 private void refreshData() { 448 // Prevent unnecessary refresh. 449 if (mRefreshDataRequired) { 450 // Mark all entries in the contact info cache as out of date, so they will be looked up 451 // again once being shown. 452 mAdapter.invalidateCache(); 453 mAdapter.setLoading(true); 454 455 fetchCalls(); 456 mCallLogQueryHandler.fetchVoicemailStatus(); 457 458 updateOnTransition(true /* onEntry */); 459 mRefreshDataRequired = false; 460 } else { 461 // Refresh the display of the existing data to update the timestamp text descriptions. 462 mAdapter.notifyDataSetChanged(); 463 } 464 } 465 466 /** 467 * Updates the call data and notification state on entering or leaving the call log tab. 468 * 469 * If we are leaving the call log tab, mark all the missed calls as read. 470 * 471 * TODO: Move to CallLogActivity 472 */ 473 private void updateOnTransition(boolean onEntry) { 474 // We don't want to update any call data when keyguard is on because the user has likely not 475 // seen the new calls yet. 476 // This might be called before onCreate() and thus we need to check null explicitly. 477 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 478 // On either of the transitions we update the missed call and voicemail notifications. 479 // While exiting we additionally consume all missed calls (by marking them as read). 480 mCallLogQueryHandler.markNewCallsAsOld(); 481 if (!onEntry) { 482 mCallLogQueryHandler.markMissedCallsAsRead(); 483 } 484 CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); 485 CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); 486 } 487 } 488 489 @Override 490 public void onEmptyViewActionButtonClicked() { 491 final Activity activity = getActivity(); 492 if (activity == null) { 493 return; 494 } 495 496 if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { 497 requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); 498 } else if (mIsRecentsFragment) { 499 // Show dialpad if we are the recents fragment. 500 ((HostInterface) activity).showDialpad(); 501 } 502 } 503 504 @Override 505 public void onRequestPermissionsResult(int requestCode, String[] permissions, 506 int[] grantResults) { 507 if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { 508 if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { 509 // Force a refresh of the data since we were missing the permission before this. 510 mRefreshDataRequired = true; 511 } 512 } 513 } 514} 515