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