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