1 /* 2 * Copyright (C) 2013 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 package com.android.dialer.list; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorSet; 20 import android.animation.ArgbEvaluator; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.app.Activity; 24 import android.app.Fragment; 25 import android.app.LoaderManager; 26 import android.content.Context; 27 import android.content.CursorLoader; 28 import android.content.Loader; 29 import android.content.SharedPreferences; 30 import android.database.Cursor; 31 import android.graphics.Rect; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.provider.CallLog; 35 import android.util.Log; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewTreeObserver; 39 import android.view.View.OnClickListener; 40 import android.view.ViewGroup; 41 import android.widget.AbsListView; 42 import android.widget.AdapterView; 43 import android.widget.AdapterView.OnItemClickListener; 44 import android.widget.ImageView; 45 import android.widget.ListView; 46 import android.widget.RelativeLayout; 47 import android.widget.RelativeLayout.LayoutParams; 48 49 import com.android.contacts.common.ContactPhotoManager; 50 import com.android.contacts.common.ContactTileLoaderFactory; 51 import com.android.contacts.common.GeoUtil; 52 import com.android.contacts.common.list.ContactEntry; 53 import com.android.contacts.common.list.ContactTileView; 54 import com.android.dialer.DialtactsActivity; 55 import com.android.dialer.R; 56 import com.android.dialer.calllog.CallLogQuery; 57 import com.android.dialer.calllog.ContactInfoHelper; 58 import com.android.dialer.calllog.CallLogAdapter; 59 import com.android.dialer.calllog.CallLogQueryHandler; 60 import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow; 61 import com.android.dialerbind.ObjectFactory; 62 63 import java.util.ArrayList; 64 import java.util.HashMap; 65 66 /** 67 * Fragment for Phone UI's favorite screen. 68 * 69 * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all" 70 * contacts. To show them at once, this merges results from {@link com.android.contacts.common.list.ContactTileAdapter} and 71 * {@link com.android.contacts.common.list.PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}. 72 * A contact filter header is also inserted between those adapters' results. 73 */ 74 public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener, 75 CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher, 76 PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener { 77 78 /** 79 * By default, the animation code assumes that all items in a list view are of the same height 80 * when animating new list items into view (e.g. from the bottom of the screen into view). 81 * This can cause incorrect translation offsets when a item that is larger or smaller than 82 * other list item is removed from the list. This key is used to provide the actual height 83 * of the removed object so that the actual translation appears correct to the user. 84 */ 85 private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; 86 87 private static final String TAG = PhoneFavoriteFragment.class.getSimpleName(); 88 private static final boolean DEBUG = false; 89 90 private int mAnimationDuration; 91 92 /** 93 * Used with LoaderManager. 94 */ 95 private static int LOADER_ID_CONTACT_TILE = 1; 96 private static int MISSED_CALL_LOADER = 2; 97 98 private static final String KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE = 99 "key_last_dismissed_call_shortcut_date"; 100 101 public interface OnShowAllContactsListener { 102 public void onShowAllContacts(); 103 } 104 105 public interface Listener { 106 public void onContactSelected(Uri contactUri); 107 public void onCallNumberDirectly(String phoneNumber); 108 } 109 110 private class MissedCallLogLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 111 112 @Override 113 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 114 final Uri uri = CallLog.Calls.CONTENT_URI; 115 final String[] projection = new String[] {CallLog.Calls.TYPE}; 116 final String selection = CallLog.Calls.TYPE + " = " + CallLog.Calls.MISSED_TYPE + 117 " AND " + CallLog.Calls.IS_READ + " = 0"; 118 return new CursorLoader(getActivity(), uri, projection, selection, null, null); 119 } 120 121 @Override 122 public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) { 123 mCallLogAdapter.setMissedCalls(data); 124 } 125 126 @Override 127 public void onLoaderReset(Loader<Cursor> cursorLoader) { 128 } 129 } 130 131 private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 132 @Override 133 public CursorLoader onCreateLoader(int id, Bundle args) { 134 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader."); 135 return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); 136 } 137 138 @Override 139 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 140 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished"); 141 mContactTileAdapter.setContactCursor(data); 142 setEmptyViewVisibility(mContactTileAdapter.getCount() == 0); 143 } 144 145 @Override 146 public void onLoaderReset(Loader<Cursor> loader) { 147 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. "); 148 } 149 } 150 151 private class ContactTileAdapterListener implements ContactTileView.Listener { 152 @Override 153 public void onContactSelected(Uri contactUri, Rect targetRect) { 154 if (mListener != null) { 155 mListener.onContactSelected(contactUri); 156 } 157 } 158 159 @Override 160 public void onCallNumberDirectly(String phoneNumber) { 161 if (mListener != null) { 162 mListener.onCallNumberDirectly(phoneNumber); 163 } 164 } 165 166 @Override 167 public int getApproximateTileWidth() { 168 return getView().getWidth() / mContactTileAdapter.getColumnCount(); 169 } 170 } 171 172 private class ScrollListener implements ListView.OnScrollListener { 173 @Override 174 public void onScroll(AbsListView view, 175 int firstVisibleItem, int visibleItemCount, int totalItemCount) { 176 } 177 178 @Override 179 public void onScrollStateChanged(AbsListView view, int scrollState) { 180 mActivityScrollListener.onListFragmentScrollStateChange(scrollState); 181 } 182 } 183 184 private Listener mListener; 185 186 private OnListFragmentScrolledListener mActivityScrollListener; 187 private OnShowAllContactsListener mShowAllContactsListener; 188 private PhoneFavoriteMergedAdapter mAdapter; 189 private PhoneFavoritesTileAdapter mContactTileAdapter; 190 191 private CallLogAdapter mCallLogAdapter; 192 private CallLogQueryHandler mCallLogQueryHandler; 193 194 private View mParentView; 195 196 private PhoneFavoriteListView mListView; 197 198 private View mShowAllContactsButton; 199 private View mShowAllContactsInEmptyViewButton; 200 private View mContactTileFrame; 201 202 private TileInteractionTeaserView mTileInteractionTeaserView; 203 204 private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>(); 205 private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>(); 206 207 /** 208 * Layout used when there are no favorites. 209 */ 210 private View mEmptyView; 211 212 /** 213 * Call shortcuts older than this date (persisted in shared preferences) will not show up in 214 * at the top of the screen 215 */ 216 private long mLastCallShortcutDate = 0; 217 218 /** 219 * The date of the current call shortcut that is showing on screen. 220 */ 221 private long mCurrentCallShortcutDate = 0; 222 223 private final ContactTileView.Listener mContactTileAdapterListener = 224 new ContactTileAdapterListener(); 225 private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener = 226 new ContactTileLoaderListener(); 227 private final ScrollListener mScrollListener = new ScrollListener(); 228 229 @Override 230 public void onAttach(Activity activity) { 231 if (DEBUG) Log.d(TAG, "onAttach()"); 232 super.onAttach(activity); 233 234 // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. 235 // We don't construct the resultant adapter at this moment since it requires LayoutInflater 236 // that will be available on onCreateView(). 237 mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener, 238 this, 239 getResources().getInteger(R.integer.contact_tile_column_count_in_favorites_new), 240 1); 241 mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); 242 } 243 244 @Override 245 public void onCreate(Bundle savedState) { 246 if (DEBUG) Log.d(TAG, "onCreate()"); 247 super.onCreate(savedState); 248 249 mAnimationDuration = getResources().getInteger(R.integer.fade_duration); 250 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), 251 this, 1); 252 final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 253 mCallLogAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, 254 new ContactInfoHelper(getActivity(), currentCountryIso), true, false); 255 setHasOptionsMenu(true); 256 } 257 258 @Override 259 public void onResume() { 260 super.onResume(); 261 final SharedPreferences prefs = getActivity().getSharedPreferences( 262 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE); 263 264 mLastCallShortcutDate = prefs.getLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, 0); 265 266 fetchCalls(); 267 mCallLogAdapter.setLoading(true); 268 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); 269 } 270 271 @Override 272 public View onCreateView(LayoutInflater inflater, ViewGroup container, 273 Bundle savedInstanceState) { 274 mParentView = inflater.inflate(R.layout.phone_favorites_fragment, container, false); 275 276 mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); 277 mListView.setItemsCanFocus(true); 278 mListView.setOnItemClickListener(this); 279 mListView.setVerticalScrollBarEnabled(false); 280 mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); 281 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 282 mListView.setOnItemSwipeListener(mContactTileAdapter); 283 mListView.setOnDragDropListener(mContactTileAdapter); 284 285 final ImageView dragShadowOverlay = 286 (ImageView) mParentView.findViewById(R.id.contact_tile_drag_shadow_overlay); 287 mListView.setDragShadowOverlay(dragShadowOverlay); 288 289 mEmptyView = mParentView.findViewById(R.id.phone_no_favorites_view); 290 291 mShowAllContactsInEmptyViewButton = mParentView.findViewById( 292 R.id.show_all_contact_button_in_nofav); 293 mShowAllContactsInEmptyViewButton.setOnClickListener(new OnClickListener() { 294 @Override 295 public void onClick(View view) { 296 showAllContacts(); 297 } 298 }); 299 300 mShowAllContactsButton = inflater.inflate(R.layout.show_all_contact_button, mListView, 301 false); 302 mShowAllContactsButton.setOnClickListener(new OnClickListener() { 303 @Override 304 public void onClick(View view) { 305 showAllContacts(); 306 } 307 }); 308 309 mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); 310 311 mTileInteractionTeaserView = (TileInteractionTeaserView) inflater.inflate( 312 R.layout.tile_interactions_teaser_view, mListView, false); 313 314 mAdapter = new PhoneFavoriteMergedAdapter(getActivity(), this, mContactTileAdapter, 315 mCallLogAdapter, mShowAllContactsButton, mTileInteractionTeaserView); 316 317 mTileInteractionTeaserView.setAdapter(mAdapter); 318 319 mListView.setAdapter(mAdapter); 320 321 mListView.setOnScrollListener(mScrollListener); 322 mListView.setFastScrollEnabled(false); 323 mListView.setFastScrollAlwaysVisible(false); 324 325 return mParentView; 326 } 327 328 public boolean hasFrequents() { 329 if (mContactTileAdapter == null) return false; 330 return mContactTileAdapter.getNumFrequents() > 0; 331 } 332 333 /* package */ void setEmptyViewVisibility(final boolean visible) { 334 final int previousVisibility = mEmptyView.getVisibility(); 335 final int newVisibility = visible ? View.VISIBLE : View.GONE; 336 337 if (previousVisibility != newVisibility) { 338 final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame 339 .getLayoutParams(); 340 params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; 341 mContactTileFrame.setLayoutParams(params); 342 mEmptyView.setVisibility(newVisibility); 343 } 344 } 345 346 @Override 347 public void onStart() { 348 super.onStart(); 349 350 final Activity activity = getActivity(); 351 352 try { 353 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 354 } catch (ClassCastException e) { 355 throw new ClassCastException(activity.toString() 356 + " must implement OnListFragmentScrolledListener"); 357 } 358 359 try { 360 mShowAllContactsListener = (OnShowAllContactsListener) activity; 361 } catch (ClassCastException e) { 362 throw new ClassCastException(activity.toString() 363 + " must implement OnShowAllContactsListener"); 364 } 365 366 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 367 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 368 // be called, on which we'll check if "all" contacts should be reloaded again or not. 369 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 370 getLoaderManager().initLoader(MISSED_CALL_LOADER, null, new MissedCallLogLoaderListener()); 371 } 372 373 /** 374 * {@inheritDoc} 375 * 376 * This is only effective for elements provided by {@link #mContactTileAdapter}. 377 * {@link #mContactTileAdapter} has its own logic for click events. 378 */ 379 @Override 380 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 381 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 382 if (position <= contactTileAdapterCount) { 383 Log.e(TAG, "onItemClick() event for unexpected position. " 384 + "The position " + position + " is before \"all\" section. Ignored."); 385 } 386 } 387 388 /** 389 * Gets called when user click on the show all contacts button. 390 */ 391 private void showAllContacts() { 392 mShowAllContactsListener.onShowAllContacts(); 393 } 394 395 public void setListener(Listener listener) { 396 mListener = listener; 397 } 398 399 @Override 400 public void onVoicemailStatusFetched(Cursor statusCursor) { 401 // no-op 402 } 403 404 @Override 405 public void onCallsFetched(Cursor cursor) { 406 animateListView(); 407 mCallLogAdapter.setLoading(false); 408 409 // Save the date of the most recent call log item 410 if (cursor != null && cursor.moveToFirst()) { 411 mCurrentCallShortcutDate = cursor.getLong(CallLogQuery.DATE); 412 } 413 414 mCallLogAdapter.changeCursor(cursor); 415 mAdapter.notifyDataSetChanged(); 416 } 417 418 @Override 419 public void fetchCalls() { 420 mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL, mLastCallShortcutDate); 421 } 422 423 @Override 424 public void onPause() { 425 // If there are any pending contact entries that are to be removed, remove them 426 mContactTileAdapter.removePendingContactEntry(); 427 // Wipe the cache to refresh the call shortcut item. This is not that expensive because 428 // it only contains one item. 429 mCallLogAdapter.invalidateCache(); 430 super.onPause(); 431 } 432 433 /** 434 * Saves the current view offsets into memory 435 */ 436 @SuppressWarnings("unchecked") 437 private void saveOffsets(int removedItemHeight) { 438 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 439 if (DEBUG) { 440 Log.d(TAG, "Child count : " + mListView.getChildCount()); 441 } 442 for (int i = 0; i < mListView.getChildCount(); i++) { 443 final View child = mListView.getChildAt(i); 444 final int position = firstVisiblePosition + i; 445 final long itemId = mAdapter.getItemId(position); 446 final int itemViewType = mAdapter.getItemViewType(position); 447 if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP) { 448 // This is a tiled row, so save horizontal offsets instead 449 saveHorizontalOffsets((ContactTileRow) child, (ArrayList<ContactEntry>) 450 mAdapter.getItem(position)); 451 } 452 if (DEBUG) { 453 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: " 454 + child.getTop()); 455 } 456 mItemIdTopMap.put(itemId, child.getTop()); 457 } 458 459 mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); 460 } 461 462 private void saveHorizontalOffsets(ContactTileRow row, ArrayList<ContactEntry> list) { 463 for (int i = 0; i < list.size(); i++) { 464 final View child = row.getChildAt(i); 465 final ContactEntry entry = list.get(i); 466 final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id); 467 if (DEBUG) { 468 Log.d(TAG, "Saving itemId: " + itemId + " for tileview child " + i + " Left: " 469 + child.getTop()); 470 } 471 mItemIdLeftMap.put(itemId, child.getLeft()); 472 } 473 } 474 475 /* 476 * Performs a animations for a row of tiles 477 */ 478 private void performHorizontalAnimations(ContactTileRow row, ArrayList<ContactEntry> list, 479 long[] idsInPlace) { 480 if (mItemIdLeftMap.isEmpty()) { 481 return; 482 } 483 final AnimatorSet animSet = new AnimatorSet(); 484 final ArrayList<Animator> animators = new ArrayList<Animator>(); 485 for (int i = 0; i < list.size(); i++) { 486 final View child = row.getChildAt(i); 487 final ContactEntry entry = list.get(i); 488 final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id); 489 490 if (containsId(idsInPlace, itemId)) { 491 animators.add(ObjectAnimator.ofFloat( 492 child, "alpha", 0.0f, 1.0f)); 493 break; 494 } else { 495 Integer startLeft = mItemIdLeftMap.get(itemId); 496 int left = child.getLeft(); 497 if (startLeft != null) { 498 if (startLeft != left) { 499 int delta = startLeft - left; 500 if (DEBUG) { 501 Log.d(TAG, "Found itemId: " + itemId + " for tileview child " + i + 502 " Left: " + left + 503 " Delta: " + delta); 504 } 505 animators.add(ObjectAnimator.ofFloat( 506 child, "translationX", delta, 0.0f)); 507 } 508 } else { 509 // In case the last square row is pushed up from the non-square section. 510 animators.add(ObjectAnimator.ofFloat( 511 child, "translationX", left, 0.0f)); 512 } 513 } 514 } 515 if (animators.size() > 0) { 516 animSet.setDuration(mAnimationDuration).playTogether(animators); 517 animSet.start(); 518 } 519 } 520 521 /* 522 * Performs animations for the list view. If the list item is a row of tiles, horizontal 523 * animations will be performed instead. 524 */ 525 private void animateListView(final long... idsInPlace) { 526 if (mItemIdTopMap.isEmpty()) { 527 // Don't do animations if the database is being queried for the first time and 528 // the previous item offsets have not been cached, or the user hasn't done anything 529 // (dragging, swiping etc) that requires an animation. 530 return; 531 } 532 533 final int removedItemHeight = mItemIdTopMap.get(KEY_REMOVED_ITEM_HEIGHT); 534 535 final ViewTreeObserver observer = mListView.getViewTreeObserver(); 536 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 537 @SuppressWarnings("unchecked") 538 @Override 539 public boolean onPreDraw() { 540 observer.removeOnPreDrawListener(this); 541 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 542 final AnimatorSet animSet = new AnimatorSet(); 543 final ArrayList<Animator> animators = new ArrayList<Animator>(); 544 for (int i = 0; i < mListView.getChildCount(); i++) { 545 final View child = mListView.getChildAt(i); 546 int position = firstVisiblePosition + i; 547 final int itemViewType = mAdapter.getItemViewType(position); 548 if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP) { 549 // This is a tiled row, so perform horizontal animations instead 550 performHorizontalAnimations((ContactTileRow) child, ( 551 ArrayList<ContactEntry>) mAdapter.getItem(position), idsInPlace); 552 } 553 554 final long itemId = mAdapter.getItemId(position); 555 556 if (containsId(idsInPlace, itemId)) { 557 animators.add(ObjectAnimator.ofFloat( 558 child, "alpha", 0.0f, 1.0f)); 559 break; 560 } else { 561 Integer startTop = mItemIdTopMap.get(itemId); 562 final int top = child.getTop(); 563 int delta = 0; 564 if (startTop != null) { 565 if (startTop != top) { 566 delta = startTop - top; 567 } 568 } else if (!mItemIdLeftMap.containsKey(itemId)) { 569 // Animate new views along with the others. The catch is that they did 570 // not exist in the start state, so we must calculate their starting 571 // position based on neighboring views. 572 573 final int itemHeight; 574 if (removedItemHeight == 0) { 575 itemHeight = child.getHeight() + mListView.getDividerHeight(); 576 } else { 577 itemHeight = removedItemHeight; 578 } 579 startTop = top + (i > 0 ? itemHeight : -itemHeight); 580 delta = startTop - top; 581 } else { 582 // In case the first non-square row is pushed down 583 // from the square section. 584 animators.add(ObjectAnimator.ofFloat( 585 child, "alpha", 0.0f, 1.0f)); 586 } 587 if (DEBUG) { 588 Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i + 589 " Top: " + top + 590 " Delta: " + delta); 591 } 592 593 if (delta != 0) { 594 animators.add(ObjectAnimator.ofFloat( 595 child, "translationY", delta, 0.0f)); 596 } 597 } 598 } 599 600 if (animators.size() > 0) { 601 animSet.setDuration(mAnimationDuration).playTogether(animators); 602 animSet.start(); 603 } 604 605 mItemIdTopMap.clear(); 606 mItemIdLeftMap.clear(); 607 return true; 608 } 609 }); 610 } 611 612 private boolean containsId(long[] ids, long target) { 613 // Linear search on array is fine because this is typically only 0-1 elements long 614 for (int i = 0; i < ids.length; i++) { 615 if (ids[i] == target) { 616 return true; 617 } 618 } 619 return false; 620 } 621 622 @Override 623 public void onDataSetChangedForAnimation(long... idsInPlace) { 624 animateListView(idsInPlace); 625 } 626 627 @Override 628 public void cacheOffsetsForDatasetChange() { 629 saveOffsets(0); 630 } 631 632 public void dismissShortcut(int height) { 633 saveOffsets(height); 634 mLastCallShortcutDate = mCurrentCallShortcutDate; 635 final SharedPreferences prefs = getActivity().getSharedPreferences( 636 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE); 637 prefs.edit().putLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, mLastCallShortcutDate) 638 .apply(); 639 fetchCalls(); 640 } 641 } 642