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.app.list; 17 18 import static android.Manifest.permission.READ_CONTACTS; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.app.Activity; 24 import android.app.Fragment; 25 import android.app.LoaderManager; 26 import android.content.CursorLoader; 27 import android.content.Loader; 28 import android.content.pm.PackageManager; 29 import android.database.Cursor; 30 import android.graphics.Rect; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Trace; 34 import android.support.v13.app.FragmentCompat; 35 import android.support.v4.util.LongSparseArray; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.animation.AnimationUtils; 40 import android.view.animation.LayoutAnimationController; 41 import android.widget.AbsListView; 42 import android.widget.AdapterView; 43 import android.widget.AdapterView.OnItemClickListener; 44 import android.widget.FrameLayout; 45 import android.widget.FrameLayout.LayoutParams; 46 import android.widget.ImageView; 47 import android.widget.ListView; 48 import com.android.contacts.common.ContactPhotoManager; 49 import com.android.contacts.common.ContactTileLoaderFactory; 50 import com.android.contacts.common.list.ContactTileView; 51 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 52 import com.android.dialer.app.R; 53 import com.android.dialer.app.widget.EmptyContentView; 54 import com.android.dialer.callintent.CallInitiationType; 55 import com.android.dialer.callintent.CallSpecificAppData; 56 import com.android.dialer.common.LogUtil; 57 import com.android.dialer.util.PermissionsUtil; 58 import com.android.dialer.util.ViewUtil; 59 import java.util.ArrayList; 60 61 /** This fragment displays the user's favorite/frequent contacts in a grid. */ 62 public class OldSpeedDialFragment extends Fragment 63 implements OnItemClickListener, 64 PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener, 65 EmptyContentView.OnEmptyViewActionButtonClickedListener, 66 FragmentCompat.OnRequestPermissionsResultCallback { 67 68 private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; 69 70 /** 71 * By default, the animation code assumes that all items in a list view are of the same height 72 * when animating new list items into view (e.g. from the bottom of the screen into view). This 73 * can cause incorrect translation offsets when a item that is larger or smaller than other list 74 * item is removed from the list. This key is used to provide the actual height of the removed 75 * object so that the actual translation appears correct to the user. 76 */ 77 private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; 78 79 private static final String TAG = "OldSpeedDialFragment"; 80 private static final boolean DEBUG = false; 81 /** Used with LoaderManager. */ 82 private static final int LOADER_ID_CONTACT_TILE = 1; 83 84 private final LongSparseArray<Integer> mItemIdTopMap = new LongSparseArray<>(); 85 private final LongSparseArray<Integer> mItemIdLeftMap = new LongSparseArray<>(); 86 private final ContactTileView.Listener mContactTileAdapterListener = 87 new ContactTileAdapterListener(); 88 private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener = 89 new ContactTileLoaderListener(); 90 private final ScrollListener mScrollListener = new ScrollListener(); 91 private int mAnimationDuration; 92 private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener; 93 private OnListFragmentScrolledListener mActivityScrollListener; 94 private PhoneFavoritesTileAdapter mContactTileAdapter; 95 private View mParentView; 96 private PhoneFavoriteListView mListView; 97 private View mContactTileFrame; 98 /** Layout used when there are no favorites. */ 99 private EmptyContentView mEmptyView; 100 101 @Override onCreate(Bundle savedState)102 public void onCreate(Bundle savedState) { 103 if (DEBUG) { 104 LogUtil.d("OldSpeedDialFragment.onCreate", null); 105 } 106 Trace.beginSection(TAG + " onCreate"); 107 super.onCreate(savedState); 108 109 // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. 110 // We don't construct the resultant adapter at this moment since it requires LayoutInflater 111 // that will be available on onCreateView(). 112 mContactTileAdapter = 113 new PhoneFavoritesTileAdapter(getActivity(), mContactTileAdapterListener, this); 114 mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity())); 115 mAnimationDuration = getResources().getInteger(R.integer.fade_duration); 116 Trace.endSection(); 117 } 118 119 @Override onResume()120 public void onResume() { 121 Trace.beginSection(TAG + " onResume"); 122 super.onResume(); 123 if (mContactTileAdapter != null) { 124 mContactTileAdapter.refreshContactsPreferences(); 125 } 126 if (PermissionsUtil.hasContactsReadPermissions(getActivity())) { 127 if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) { 128 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 129 130 } else { 131 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); 132 } 133 134 mEmptyView.setDescription(R.string.speed_dial_empty); 135 mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action); 136 } else { 137 mEmptyView.setDescription(R.string.permission_no_speeddial); 138 mEmptyView.setActionLabel(R.string.permission_single_turn_on); 139 } 140 Trace.endSection(); 141 } 142 143 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)144 public View onCreateView( 145 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 146 Trace.beginSection(TAG + " onCreateView"); 147 mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); 148 149 mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); 150 mListView.setOnItemClickListener(this); 151 mListView.setVerticalScrollBarEnabled(false); 152 mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); 153 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 154 mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter); 155 156 final ImageView dragShadowOverlay = 157 (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); 158 mListView.setDragShadowOverlay(dragShadowOverlay); 159 160 mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view); 161 mEmptyView.setImage(R.drawable.empty_speed_dial); 162 mEmptyView.setActionClickedListener(this); 163 164 mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); 165 166 final LayoutAnimationController controller = 167 new LayoutAnimationController( 168 AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); 169 controller.setDelay(0); 170 mListView.setLayoutAnimation(controller); 171 mListView.setAdapter(mContactTileAdapter); 172 173 mListView.setOnScrollListener(mScrollListener); 174 mListView.setFastScrollEnabled(false); 175 mListView.setFastScrollAlwaysVisible(false); 176 177 //prevent content changes of the list from firing accessibility events. 178 mListView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); 179 ContentChangedFilter.addToParent(mListView); 180 181 Trace.endSection(); 182 return mParentView; 183 } 184 hasFrequents()185 public boolean hasFrequents() { 186 if (mContactTileAdapter == null) { 187 return false; 188 } 189 return mContactTileAdapter.getNumFrequents() > 0; 190 } 191 setEmptyViewVisibility(final boolean visible)192 /* package */ void setEmptyViewVisibility(final boolean visible) { 193 final int previousVisibility = mEmptyView.getVisibility(); 194 final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE; 195 final int listViewVisibility = visible ? View.GONE : View.VISIBLE; 196 197 if (previousVisibility != emptyViewVisibility) { 198 final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame.getLayoutParams(); 199 params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; 200 mContactTileFrame.setLayoutParams(params); 201 mEmptyView.setVisibility(emptyViewVisibility); 202 mListView.setVisibility(listViewVisibility); 203 } 204 } 205 206 @Override onStart()207 public void onStart() { 208 super.onStart(); 209 210 final Activity activity = getActivity(); 211 212 try { 213 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 214 } catch (ClassCastException e) { 215 throw new ClassCastException( 216 activity.toString() + " must implement OnListFragmentScrolledListener"); 217 } 218 219 try { 220 OnDragDropListener listener = (OnDragDropListener) activity; 221 mListView.getDragDropController().addOnDragDropListener(listener); 222 ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); 223 } catch (ClassCastException e) { 224 throw new ClassCastException( 225 activity.toString() + " must implement OnDragDropListener and HostInterface"); 226 } 227 228 try { 229 mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; 230 } catch (ClassCastException e) { 231 throw new ClassCastException( 232 activity.toString() + " must implement PhoneFavoritesFragment.listener"); 233 } 234 235 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 236 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 237 // be called, on which we'll check if "all" contacts should be reloaded again or not. 238 if (PermissionsUtil.hasContactsReadPermissions(activity)) { 239 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 240 } else { 241 setEmptyViewVisibility(true); 242 } 243 } 244 245 /** 246 * {@inheritDoc} 247 * 248 * <p>This is only effective for elements provided by {@link #mContactTileAdapter}. {@link 249 * #mContactTileAdapter} has its own logic for click events. 250 */ 251 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)252 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 253 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 254 if (position <= contactTileAdapterCount) { 255 LogUtil.e( 256 "OldSpeedDialFragment.onItemClick", 257 "event for unexpected position. The position " 258 + position 259 + " is before \"all\" section. Ignored."); 260 } 261 } 262 263 /** 264 * Cache the current view offsets into memory. Once a relayout of views in the ListView has 265 * happened due to a dataset change, the cached offsets are used to create animations that slide 266 * views from their previous positions to their new ones, to give the appearance that the views 267 * are sliding into their new positions. 268 */ saveOffsets(int removedItemHeight)269 private void saveOffsets(int removedItemHeight) { 270 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 271 if (DEBUG) { 272 LogUtil.d("OldSpeedDialFragment.saveOffsets", "Child count : " + mListView.getChildCount()); 273 } 274 for (int i = 0; i < mListView.getChildCount(); i++) { 275 final View child = mListView.getChildAt(i); 276 final int position = firstVisiblePosition + i; 277 // Since we are getting the position from mListView and then querying 278 // mContactTileAdapter, its very possible that things are out of sync 279 // and we might index out of bounds. Let's make sure that this doesn't happen. 280 if (!mContactTileAdapter.isIndexInBound(position)) { 281 continue; 282 } 283 final long itemId = mContactTileAdapter.getItemId(position); 284 if (DEBUG) { 285 LogUtil.d( 286 "OldSpeedDialFragment.saveOffsets", 287 "Saving itemId: " + itemId + " for listview child " + i + " Top: " + child.getTop()); 288 } 289 mItemIdTopMap.put(itemId, child.getTop()); 290 mItemIdLeftMap.put(itemId, child.getLeft()); 291 } 292 mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); 293 } 294 295 /* 296 * Performs animations for the gridView 297 */ animateGridView(final long... idsInPlace)298 private void animateGridView(final long... idsInPlace) { 299 if (mItemIdTopMap.size() == 0) { 300 // Don't do animations if the database is being queried for the first time and 301 // the previous item offsets have not been cached, or the user hasn't done anything 302 // (dragging, swiping etc) that requires an animation. 303 return; 304 } 305 306 ViewUtil.doOnPreDraw( 307 mListView, 308 true, 309 new Runnable() { 310 @Override 311 public void run() { 312 313 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 314 final AnimatorSet animSet = new AnimatorSet(); 315 final ArrayList<Animator> animators = new ArrayList<Animator>(); 316 for (int i = 0; i < mListView.getChildCount(); i++) { 317 final View child = mListView.getChildAt(i); 318 int position = firstVisiblePosition + i; 319 320 // Since we are getting the position from mListView and then querying 321 // mContactTileAdapter, its very possible that things are out of sync 322 // and we might index out of bounds. Let's make sure that this doesn't happen. 323 if (!mContactTileAdapter.isIndexInBound(position)) { 324 continue; 325 } 326 327 final long itemId = mContactTileAdapter.getItemId(position); 328 329 if (containsId(idsInPlace, itemId)) { 330 animators.add(ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f)); 331 break; 332 } else { 333 Integer startTop = mItemIdTopMap.get(itemId); 334 Integer startLeft = mItemIdLeftMap.get(itemId); 335 final int top = child.getTop(); 336 final int left = child.getLeft(); 337 int deltaX = 0; 338 int deltaY = 0; 339 340 if (startLeft != null) { 341 if (startLeft != left) { 342 deltaX = startLeft - left; 343 animators.add(ObjectAnimator.ofFloat(child, "translationX", deltaX, 0.0f)); 344 } 345 } 346 347 if (startTop != null) { 348 if (startTop != top) { 349 deltaY = startTop - top; 350 animators.add(ObjectAnimator.ofFloat(child, "translationY", deltaY, 0.0f)); 351 } 352 } 353 354 if (DEBUG) { 355 LogUtil.d( 356 "OldSpeedDialFragment.onPreDraw", 357 "Found itemId: " 358 + itemId 359 + " for listview child " 360 + i 361 + " Top: " 362 + top 363 + " Delta: " 364 + deltaY); 365 } 366 } 367 } 368 369 if (animators.size() > 0) { 370 animSet.setDuration(mAnimationDuration).playTogether(animators); 371 animSet.start(); 372 } 373 374 mItemIdTopMap.clear(); 375 mItemIdLeftMap.clear(); 376 } 377 }); 378 } 379 containsId(long[] ids, long target)380 private boolean containsId(long[] ids, long target) { 381 // Linear search on array is fine because this is typically only 0-1 elements long 382 for (int i = 0; i < ids.length; i++) { 383 if (ids[i] == target) { 384 return true; 385 } 386 } 387 return false; 388 } 389 390 @Override onDataSetChangedForAnimation(long... idsInPlace)391 public void onDataSetChangedForAnimation(long... idsInPlace) { 392 animateGridView(idsInPlace); 393 } 394 395 @Override cacheOffsetsForDatasetChange()396 public void cacheOffsetsForDatasetChange() { 397 saveOffsets(0); 398 } 399 400 @Override onEmptyViewActionButtonClicked()401 public void onEmptyViewActionButtonClicked() { 402 final Activity activity = getActivity(); 403 if (activity == null) { 404 return; 405 } 406 407 if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { 408 FragmentCompat.requestPermissions( 409 this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE); 410 } else { 411 // Switch tabs 412 ((HostInterface) activity).showAllContactsTab(); 413 } 414 } 415 416 @Override onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)417 public void onRequestPermissionsResult( 418 int requestCode, String[] permissions, int[] grantResults) { 419 if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { 420 if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { 421 PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS); 422 } 423 } 424 } 425 426 public interface HostInterface { 427 setDragDropController(DragDropController controller)428 void setDragDropController(DragDropController controller); 429 showAllContactsTab()430 void showAllContactsTab(); 431 } 432 433 private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 434 435 @Override onCreateLoader(int id, Bundle args)436 public CursorLoader onCreateLoader(int id, Bundle args) { 437 if (DEBUG) { 438 LogUtil.d("ContactTileLoaderListener.onCreateLoader", null); 439 } 440 return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); 441 } 442 443 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)444 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 445 if (DEBUG) { 446 LogUtil.d("ContactTileLoaderListener.onLoadFinished", null); 447 } 448 mContactTileAdapter.setContactCursor(data); 449 setEmptyViewVisibility(mContactTileAdapter.getCount() == 0); 450 } 451 452 @Override onLoaderReset(Loader<Cursor> loader)453 public void onLoaderReset(Loader<Cursor> loader) { 454 if (DEBUG) { 455 LogUtil.d("ContactTileLoaderListener.onLoaderReset", null); 456 } 457 } 458 } 459 460 private class ContactTileAdapterListener implements ContactTileView.Listener { 461 462 @Override onContactSelected(Uri contactUri, Rect targetRect)463 public void onContactSelected(Uri contactUri, Rect targetRect) { 464 if (mPhoneNumberPickerActionListener != null) { 465 CallSpecificAppData callSpecificAppData = 466 CallSpecificAppData.newBuilder() 467 .setCallInitiationType(CallInitiationType.Type.SPEED_DIAL) 468 .build(); 469 mPhoneNumberPickerActionListener.onPickDataUri( 470 contactUri, false /* isVideoCall */, callSpecificAppData); 471 } 472 } 473 474 @Override onCallNumberDirectly(String phoneNumber)475 public void onCallNumberDirectly(String phoneNumber) { 476 if (mPhoneNumberPickerActionListener != null) { 477 CallSpecificAppData callSpecificAppData = 478 CallSpecificAppData.newBuilder() 479 .setCallInitiationType(CallInitiationType.Type.SPEED_DIAL) 480 .build(); 481 mPhoneNumberPickerActionListener.onPickPhoneNumber( 482 phoneNumber, false /* isVideoCall */, callSpecificAppData); 483 } 484 } 485 } 486 487 private class ScrollListener implements ListView.OnScrollListener { 488 489 @Override onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)490 public void onScroll( 491 AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 492 if (mActivityScrollListener != null) { 493 mActivityScrollListener.onListFragmentScroll( 494 firstVisibleItem, visibleItemCount, totalItemCount); 495 } 496 } 497 498 @Override onScrollStateChanged(AbsListView view, int scrollState)499 public void onScrollStateChanged(AbsListView view, int scrollState) { 500 mActivityScrollListener.onListFragmentScrollStateChange(scrollState); 501 } 502 } 503 } 504