blob: 8e0f89028a2aa7eee70880a2cf27d66f050d3952 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.app.list;
import static android.Manifest.permission.READ_CONTACTS;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.Nullable;
import android.support.v13.app.FragmentCompat;
import android.support.v4.util.LongSparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
import android.widget.ListView;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactTileLoaderFactory;
import com.android.contacts.common.list.ContactTileView;
import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
import com.android.dialer.app.R;
import com.android.dialer.app.list.ListsFragment.ListsPage;
import com.android.dialer.app.widget.EmptyContentView;
import com.android.dialer.callintent.nano.CallInitiationType;
import com.android.dialer.callintent.nano.CallSpecificAppData;
import com.android.dialer.common.LogUtil;
import com.android.dialer.util.PermissionsUtil;
import com.android.dialer.util.ViewUtil;
import java.util.ArrayList;
/** This fragment displays the user's favorite/frequent contacts in a grid. */
public class SpeedDialFragment extends Fragment
implements ListsPage,
OnItemClickListener,
PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener,
EmptyContentView.OnEmptyViewActionButtonClickedListener,
FragmentCompat.OnRequestPermissionsResultCallback {
private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
/**
* By default, the animation code assumes that all items in a list view are of the same height
* when animating new list items into view (e.g. from the bottom of the screen into view). This
* can cause incorrect translation offsets when a item that is larger or smaller than other list
* item is removed from the list. This key is used to provide the actual height of the removed
* object so that the actual translation appears correct to the user.
*/
private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE;
private static final String TAG = "SpeedDialFragment";
private static final boolean DEBUG = false;
/** Used with LoaderManager. */
private static final int LOADER_ID_CONTACT_TILE = 1;
private final LongSparseArray<Integer> mItemIdTopMap = new LongSparseArray<>();
private final LongSparseArray<Integer> mItemIdLeftMap = new LongSparseArray<>();
private final ContactTileView.Listener mContactTileAdapterListener =
new ContactTileAdapterListener();
private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
new ContactTileLoaderListener();
private final ScrollListener mScrollListener = new ScrollListener();
private int mAnimationDuration;
private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener;
private OnListFragmentScrolledListener mActivityScrollListener;
private PhoneFavoritesTileAdapter mContactTileAdapter;
private View mParentView;
private PhoneFavoriteListView mListView;
private View mContactTileFrame;
/** Layout used when there are no favorites. */
private EmptyContentView mEmptyView;
@Override
public void onCreate(Bundle savedState) {
if (DEBUG) {
LogUtil.d("SpeedDialFragment.onCreate", null);
}
Trace.beginSection(TAG + " onCreate");
super.onCreate(savedState);
// Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
// We don't construct the resultant adapter at this moment since it requires LayoutInflater
// that will be available on onCreateView().
mContactTileAdapter =
new PhoneFavoritesTileAdapter(getActivity(), mContactTileAdapterListener, this);
mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity()));
mAnimationDuration = getResources().getInteger(R.integer.fade_duration);
Trace.endSection();
}
@Override
public void onResume() {
Trace.beginSection(TAG + " onResume");
super.onResume();
if (mContactTileAdapter != null) {
mContactTileAdapter.refreshContactsPreferences();
}
if (PermissionsUtil.hasContactsPermissions(getActivity())) {
if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) {
getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
} else {
getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
}
mEmptyView.setDescription(R.string.speed_dial_empty);
mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action);
} else {
mEmptyView.setDescription(R.string.permission_no_speeddial);
mEmptyView.setActionLabel(R.string.permission_single_turn_on);
}
Trace.endSection();
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Trace.beginSection(TAG + " onCreateView");
mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false);
mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list);
mListView.setOnItemClickListener(this);
mListView.setVerticalScrollBarEnabled(false);
mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter);
final ImageView dragShadowOverlay =
(ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay);
mListView.setDragShadowOverlay(dragShadowOverlay);
mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view);
mEmptyView.setImage(R.drawable.empty_speed_dial);
mEmptyView.setActionClickedListener(this);
mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame);
final LayoutAnimationController controller =
new LayoutAnimationController(
AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in));
controller.setDelay(0);
mListView.setLayoutAnimation(controller);
mListView.setAdapter(mContactTileAdapter);
mListView.setOnScrollListener(mScrollListener);
mListView.setFastScrollEnabled(false);
mListView.setFastScrollAlwaysVisible(false);
//prevent content changes of the list from firing accessibility events.
mListView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE);
ContentChangedFilter.addToParent(mListView);
Trace.endSection();
return mParentView;
}
public boolean hasFrequents() {
if (mContactTileAdapter == null) {
return false;
}
return mContactTileAdapter.getNumFrequents() > 0;
}
/* package */ void setEmptyViewVisibility(final boolean visible) {
final int previousVisibility = mEmptyView.getVisibility();
final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE;
final int listViewVisibility = visible ? View.GONE : View.VISIBLE;
if (previousVisibility != emptyViewVisibility) {
final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame.getLayoutParams();
params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
mContactTileFrame.setLayoutParams(params);
mEmptyView.setVisibility(emptyViewVisibility);
mListView.setVisibility(listViewVisibility);
}
}
@Override
public void onStart() {
super.onStart();
final Activity activity = getActivity();
try {
mActivityScrollListener = (OnListFragmentScrolledListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement OnListFragmentScrolledListener");
}
try {
OnDragDropListener listener = (OnDragDropListener) activity;
mListView.getDragDropController().addOnDragDropListener(listener);
((HostInterface) activity).setDragDropController(mListView.getDragDropController());
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement OnDragDropListener and HostInterface");
}
try {
mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement PhoneFavoritesFragment.listener");
}
// Use initLoader() instead of restartLoader() to refraining unnecessary reload.
// This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
// be called, on which we'll check if "all" contacts should be reloaded again or not.
if (PermissionsUtil.hasContactsPermissions(activity)) {
getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
} else {
setEmptyViewVisibility(true);
}
}
/**
* {@inheritDoc}
*
* <p>This is only effective for elements provided by {@link #mContactTileAdapter}. {@link
* #mContactTileAdapter} has its own logic for click events.
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final int contactTileAdapterCount = mContactTileAdapter.getCount();
if (position <= contactTileAdapterCount) {
LogUtil.e(
"SpeedDialFragment.onItemClick",
"event for unexpected position. The position "
+ position
+ " is before \"all\" section. Ignored.");
}
}
/**
* Cache the current view offsets into memory. Once a relayout of views in the ListView has
* happened due to a dataset change, the cached offsets are used to create animations that slide
* views from their previous positions to their new ones, to give the appearance that the views
* are sliding into their new positions.
*/
private void saveOffsets(int removedItemHeight) {
final int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (DEBUG) {
LogUtil.d("SpeedDialFragment.saveOffsets", "Child count : " + mListView.getChildCount());
}
for (int i = 0; i < mListView.getChildCount(); i++) {
final View child = mListView.getChildAt(i);
final int position = firstVisiblePosition + i;
// Since we are getting the position from mListView and then querying
// mContactTileAdapter, its very possible that things are out of sync
// and we might index out of bounds. Let's make sure that this doesn't happen.
if (!mContactTileAdapter.isIndexInBound(position)) {
continue;
}
final long itemId = mContactTileAdapter.getItemId(position);
if (DEBUG) {
LogUtil.d(
"SpeedDialFragment.saveOffsets",
"Saving itemId: " + itemId + " for listview child " + i + " Top: " + child.getTop());
}
mItemIdTopMap.put(itemId, child.getTop());
mItemIdLeftMap.put(itemId, child.getLeft());
}
mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight);
}
/*
* Performs animations for the gridView
*/
private void animateGridView(final long... idsInPlace) {
if (mItemIdTopMap.size() == 0) {
// Don't do animations if the database is being queried for the first time and
// the previous item offsets have not been cached, or the user hasn't done anything
// (dragging, swiping etc) that requires an animation.
return;
}
ViewUtil.doOnPreDraw(
mListView,
true,
new Runnable() {
@Override
public void run() {
final int firstVisiblePosition = mListView.getFirstVisiblePosition();
final AnimatorSet animSet = new AnimatorSet();
final ArrayList<Animator> animators = new ArrayList<Animator>();
for (int i = 0; i < mListView.getChildCount(); i++) {
final View child = mListView.getChildAt(i);
int position = firstVisiblePosition + i;
// Since we are getting the position from mListView and then querying
// mContactTileAdapter, its very possible that things are out of sync
// and we might index out of bounds. Let's make sure that this doesn't happen.
if (!mContactTileAdapter.isIndexInBound(position)) {
continue;
}
final long itemId = mContactTileAdapter.getItemId(position);
if (containsId(idsInPlace, itemId)) {
animators.add(ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f));
break;
} else {
Integer startTop = mItemIdTopMap.get(itemId);
Integer startLeft = mItemIdLeftMap.get(itemId);
final int top = child.getTop();
final int left = child.getLeft();
int deltaX = 0;
int deltaY = 0;
if (startLeft != null) {
if (startLeft != left) {
deltaX = startLeft - left;
animators.add(ObjectAnimator.ofFloat(child, "translationX", deltaX, 0.0f));
}
}
if (startTop != null) {
if (startTop != top) {
deltaY = startTop - top;
animators.add(ObjectAnimator.ofFloat(child, "translationY", deltaY, 0.0f));
}
}
if (DEBUG) {
LogUtil.d(
"SpeedDialFragment.onPreDraw",
"Found itemId: "
+ itemId
+ " for listview child "
+ i
+ " Top: "
+ top
+ " Delta: "
+ deltaY);
}
}
}
if (animators.size() > 0) {
animSet.setDuration(mAnimationDuration).playTogether(animators);
animSet.start();
}
mItemIdTopMap.clear();
mItemIdLeftMap.clear();
}
});
}
private boolean containsId(long[] ids, long target) {
// Linear search on array is fine because this is typically only 0-1 elements long
for (int i = 0; i < ids.length; i++) {
if (ids[i] == target) {
return true;
}
}
return false;
}
@Override
public void onDataSetChangedForAnimation(long... idsInPlace) {
animateGridView(idsInPlace);
}
@Override
public void cacheOffsetsForDatasetChange() {
saveOffsets(0);
}
@Override
public void onEmptyViewActionButtonClicked() {
final Activity activity = getActivity();
if (activity == null) {
return;
}
if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
FragmentCompat.requestPermissions(
this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
} else {
// Switch tabs
((HostInterface) activity).showAllContactsTab();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS);
}
}
}
@Override
public void onPageResume(@Nullable Activity activity) {
LogUtil.i("SpeedDialFragment.onPageResume", null);
}
@Override
public void onPagePause(@Nullable Activity activity) {
LogUtil.i("SpeedDialFragment.onPagePause", null);
}
public interface HostInterface {
void setDragDropController(DragDropController controller);
void showAllContactsTab();
}
private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public CursorLoader onCreateLoader(int id, Bundle args) {
if (DEBUG) {
LogUtil.d("ContactTileLoaderListener.onCreateLoader", null);
}
return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (DEBUG) {
LogUtil.d("ContactTileLoaderListener.onLoadFinished", null);
}
mContactTileAdapter.setContactCursor(data);
setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (DEBUG) {
LogUtil.d("ContactTileLoaderListener.onLoaderReset", null);
}
}
}
private class ContactTileAdapterListener implements ContactTileView.Listener {
@Override
public void onContactSelected(Uri contactUri, Rect targetRect) {
if (mPhoneNumberPickerActionListener != null) {
CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL;
mPhoneNumberPickerActionListener.onPickDataUri(
contactUri, false /* isVideoCall */, callSpecificAppData);
}
}
@Override
public void onCallNumberDirectly(String phoneNumber) {
if (mPhoneNumberPickerActionListener != null) {
CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL;
mPhoneNumberPickerActionListener.onPickPhoneNumber(
phoneNumber, false /* isVideoCall */, callSpecificAppData);
}
}
}
private class ScrollListener implements ListView.OnScrollListener {
@Override
public void onScroll(
AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mActivityScrollListener != null) {
mActivityScrollListener.onListFragmentScroll(
firstVisibleItem, visibleItemCount, totalItemCount);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
}
}
}