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