Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2015 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 
     17 package com.android.contacts.list;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.graphics.drawable.Drawable;
     22 import android.os.Bundle;
     23 import android.provider.ContactsContract;
     24 import android.support.v4.view.ViewCompat;
     25 import android.util.Log;
     26 import android.view.LayoutInflater;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.accessibility.AccessibilityEvent;
     30 import android.view.animation.Animation;
     31 import android.view.animation.AnimationUtils;
     32 import android.widget.AbsListView;
     33 import android.widget.ImageView;
     34 import android.widget.TextView;
     35 
     36 import com.android.contacts.R;
     37 import com.android.contacts.activities.ActionBarAdapter;
     38 import com.android.contacts.group.GroupMembersFragment;
     39 import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener;
     40 import com.android.contacts.logging.ListEvent.ActionType;
     41 import com.android.contacts.logging.Logger;
     42 import com.android.contacts.logging.SearchState;
     43 import com.android.contacts.model.AccountTypeManager;
     44 import com.android.contacts.model.account.AccountType;
     45 import com.android.contacts.model.account.AccountWithDataSet;
     46 import com.android.contacts.model.account.GoogleAccountType;
     47 
     48 import java.util.ArrayList;
     49 import java.util.List;
     50 import java.util.TreeSet;
     51 
     52 /**
     53  * Fragment containing a contact list used for browsing contacts and optionally selecting
     54  * multiple contacts via checkboxes.
     55  */
     56 public abstract class MultiSelectContactsListFragment<T extends MultiSelectEntryContactListAdapter>
     57         extends ContactEntryListFragment<T>
     58         implements SelectedContactsListener {
     59 
     60     protected boolean mAnimateOnLoad;
     61     private static final String TAG = "MultiContactsList";
     62 
     63     public interface OnCheckBoxListActionListener {
     64         void onStartDisplayingCheckBoxes();
     65         void onSelectedContactIdsChanged();
     66         void onStopDisplayingCheckBoxes();
     67     }
     68 
     69     private static final String EXTRA_KEY_SELECTED_CONTACTS = "selected_contacts";
     70 
     71     private OnCheckBoxListActionListener mCheckBoxListListener;
     72 
     73     public void setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener) {
     74         mCheckBoxListListener = checkBoxListListener;
     75     }
     76 
     77     public void setAnimateOnLoad(boolean shouldAnimate) {
     78         mAnimateOnLoad = shouldAnimate;
     79     }
     80 
     81     @Override
     82     public void onSelectedContactsChanged() {
     83         if (mCheckBoxListListener != null) mCheckBoxListListener.onSelectedContactIdsChanged();
     84     }
     85 
     86     @Override
     87     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     88             Bundle savedInstanceState) {
     89         super.onCreateView(inflater, container, savedInstanceState);
     90         if (savedInstanceState == null && mAnimateOnLoad) {
     91             setLayoutAnimation(getListView(), R.anim.slide_and_fade_in_layout_animation);
     92         }
     93         return getView();
     94     }
     95 
     96     @Override
     97     public void onActivityCreated(Bundle savedInstanceState) {
     98         super.onActivityCreated(savedInstanceState);
     99         if (savedInstanceState != null) {
    100             final TreeSet<Long> selectedContactIds = (TreeSet<Long>)
    101                     savedInstanceState.getSerializable(EXTRA_KEY_SELECTED_CONTACTS);
    102             getAdapter().setSelectedContactIds(selectedContactIds);
    103         }
    104     }
    105 
    106     @Override
    107     public void onStart() {
    108         super.onStart();
    109         if (mCheckBoxListListener != null) {
    110             mCheckBoxListListener.onSelectedContactIdsChanged();
    111         }
    112     }
    113 
    114     public TreeSet<Long> getSelectedContactIds() {
    115         return getAdapter().getSelectedContactIds();
    116     }
    117 
    118     public long[] getSelectedContactIdsArray() {
    119         return getAdapter().getSelectedContactIdsArray();
    120     }
    121 
    122     @Override
    123     protected void configureAdapter() {
    124         super.configureAdapter();
    125         getAdapter().setSelectedContactsListener(this);
    126     }
    127 
    128     @Override
    129     public void onSaveInstanceState(Bundle outState) {
    130         super.onSaveInstanceState(outState);
    131         outState.putSerializable(EXTRA_KEY_SELECTED_CONTACTS, getSelectedContactIds());
    132     }
    133 
    134     public void displayCheckBoxes(boolean displayCheckBoxes) {
    135         if (getAdapter() != null) {
    136             getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
    137             if (!displayCheckBoxes) {
    138                 clearCheckBoxes();
    139             }
    140         }
    141     }
    142 
    143     public void clearCheckBoxes() {
    144         getAdapter().setSelectedContactIds(new TreeSet<Long>());
    145     }
    146 
    147     @Override
    148     protected boolean onItemLongClick(int position, long id) {
    149         final int previouslySelectedCount = getAdapter().getSelectedContactIds().size();
    150         final long contactId = getContactId(position);
    151         final int partition = getAdapter().getPartitionForPosition(position);
    152         if (contactId >= 0 && partition == ContactsContract.Directory.DEFAULT) {
    153             if (mCheckBoxListListener != null) {
    154                 mCheckBoxListListener.onStartDisplayingCheckBoxes();
    155             }
    156             getAdapter().toggleSelectionOfContactId(contactId);
    157             Logger.logListEvent(ActionType.SELECT, getListType(),
    158                     /* count */ getAdapter().getCount(), /* clickedIndex */ position,
    159                     /* numSelected */ 1);
    160             // Manually send clicked event if there is a checkbox.
    161             // See b/24098561. TalkBack will not read it otherwise.
    162             final int index = position + getListView().getHeaderViewsCount() - getListView()
    163                     .getFirstVisiblePosition();
    164             if (index >= 0 && index < getListView().getChildCount()) {
    165                 getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent
    166                         .TYPE_VIEW_CLICKED);
    167             }
    168         }
    169         final int nowSelectedCount = getAdapter().getSelectedContactIds().size();
    170         if (mCheckBoxListListener != null
    171                 && previouslySelectedCount != 0 && nowSelectedCount == 0) {
    172             // Last checkbox has been unchecked. So we should stop displaying checkboxes.
    173             mCheckBoxListListener.onStopDisplayingCheckBoxes();
    174         }
    175         return true;
    176     }
    177 
    178     @Override
    179     protected void onItemClick(int position, long id) {
    180         final long contactId = getContactId(position);
    181         if (contactId < 0) {
    182             return;
    183         }
    184         if (getAdapter().isDisplayingCheckBoxes()) {
    185             getAdapter().toggleSelectionOfContactId(contactId);
    186         }
    187         if (mCheckBoxListListener != null && getAdapter().getSelectedContactIds().size() == 0) {
    188             mCheckBoxListListener.onStopDisplayingCheckBoxes();
    189         }
    190     }
    191 
    192     private long getContactId(int position) {
    193         final int contactIdColumnIndex = getAdapter().getContactColumnIdIndex();
    194 
    195         final Cursor cursor = (Cursor) getAdapter().getItem(position);
    196         if (cursor != null) {
    197             if (cursor.getColumnCount() > contactIdColumnIndex) {
    198                 return cursor.getLong(contactIdColumnIndex);
    199             }
    200         }
    201 
    202         Log.w(TAG, "Failed to get contact ID from cursor column " + contactIdColumnIndex);
    203         return -1;
    204     }
    205 
    206     /**
    207      * Returns the state of the search results currently presented to the user.
    208      */
    209     public SearchState createSearchState() {
    210         return createSearchState(/* selectedPosition */ -1);
    211     }
    212 
    213     /**
    214      * Returns the state of the search results presented to the user
    215      * at the time the result in the given position was clicked.
    216      */
    217     public SearchState createSearchStateForSearchResultClick(int selectedPosition) {
    218         return createSearchState(selectedPosition);
    219     }
    220 
    221     private SearchState createSearchState(int selectedPosition) {
    222         final MultiSelectEntryContactListAdapter adapter = getAdapter();
    223         if (adapter == null) {
    224             return null;
    225         }
    226         final SearchState searchState = new SearchState();
    227         searchState.queryLength = adapter.getQueryString() == null
    228                 ? 0 : adapter.getQueryString().length();
    229         searchState.numPartitions = adapter.getPartitionCount();
    230 
    231         // Set the number of results displayed to the user.  Note that the adapter.getCount(),
    232         // value does not always match the number of results actually displayed to the user,
    233         // which is why we calculate it manually.
    234         final List<Integer> numResultsInEachPartition = new ArrayList<>();
    235         for (int i = 0; i < adapter.getPartitionCount(); i++) {
    236             final Cursor cursor = adapter.getCursor(i);
    237             if (cursor == null || cursor.isClosed()) {
    238                 // Something went wrong, abort.
    239                 numResultsInEachPartition.clear();
    240                 break;
    241             }
    242             numResultsInEachPartition.add(cursor.getCount());
    243         }
    244         if (!numResultsInEachPartition.isEmpty()) {
    245             int numResults = 0;
    246             for (int i = 0; i < numResultsInEachPartition.size(); i++) {
    247                 numResults += numResultsInEachPartition.get(i);
    248             }
    249             searchState.numResults = numResults;
    250         }
    251 
    252         // If a selection was made, set additional search state
    253         if (selectedPosition >= 0) {
    254             searchState.selectedPartition = adapter.getPartitionForPosition(selectedPosition);
    255             searchState.selectedIndexInPartition = adapter.getOffsetInPartition(selectedPosition);
    256             final Cursor cursor = adapter.getCursor(searchState.selectedPartition);
    257             searchState.numResultsInSelectedPartition =
    258                     cursor == null || cursor.isClosed() ? -1 : cursor.getCount();
    259 
    260             // Calculate the index across all partitions
    261             if (!numResultsInEachPartition.isEmpty()) {
    262                 int selectedIndex = 0;
    263                 for (int i = 0; i < searchState.selectedPartition; i++) {
    264                     selectedIndex += numResultsInEachPartition.get(i);
    265                 }
    266                 selectedIndex += searchState.selectedIndexInPartition;
    267                 searchState.selectedIndex = selectedIndex;
    268             }
    269         }
    270         return searchState;
    271     }
    272 
    273     protected void setLayoutAnimation(final ViewGroup view, int animationId) {
    274         if (view == null) {
    275             return;
    276         }
    277         view.setLayoutAnimationListener(new Animation.AnimationListener() {
    278             @Override
    279             public void onAnimationStart(Animation animation) {
    280             }
    281 
    282             @Override
    283             public void onAnimationEnd(Animation animation) {
    284                 view.setLayoutAnimation(null);
    285             }
    286 
    287             @Override
    288             public void onAnimationRepeat(Animation animation) {
    289             }
    290         });
    291         view.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), animationId));
    292     }
    293 
    294     @Override
    295     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    296             int totalItemCount) {
    297         final View accountFilterContainer = getView().findViewById(
    298                 R.id.account_filter_header_container);
    299         if (accountFilterContainer == null) {
    300             return;
    301         }
    302 
    303         int firstCompletelyVisibleItem = firstVisibleItem;
    304         if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) {
    305             firstCompletelyVisibleItem++;
    306         }
    307 
    308         if (firstCompletelyVisibleItem == 0) {
    309             ViewCompat.setElevation(accountFilterContainer, 0);
    310         } else {
    311             ViewCompat.setElevation(accountFilterContainer,
    312                     getResources().getDimension(R.dimen.contact_list_header_elevation));
    313         }
    314     }
    315 
    316     protected void bindListHeaderCustom(View listView, View accountFilterContainer) {
    317         bindListHeaderCommon(listView, accountFilterContainer);
    318 
    319         final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
    320                 R.id.account_filter_header);
    321         accountFilterHeader.setText(R.string.listCustomView);
    322         accountFilterHeader.setAllCaps(false);
    323 
    324         final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer
    325                 .findViewById(R.id.account_filter_icon);
    326         accountFilterHeaderIcon.setVisibility(View.GONE);
    327     }
    328 
    329     /**
    330      * Show account icon, count of contacts and account name in the header of the list.
    331      */
    332     protected void bindListHeader(Context context, View listView, View accountFilterContainer,
    333             AccountWithDataSet accountWithDataSet, int memberCount) {
    334         if (memberCount < 0) {
    335             hideHeaderAndAddPadding(context, listView, accountFilterContainer);
    336             return;
    337         }
    338 
    339         bindListHeaderCommon(listView, accountFilterContainer);
    340 
    341         final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
    342         final AccountType accountType = accountTypeManager.getAccountType(
    343                 accountWithDataSet.type, accountWithDataSet.dataSet);
    344 
    345         // Set text of count of contacts and account name
    346         final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
    347                 R.id.account_filter_header);
    348         final String headerText = shouldShowAccountName(accountType)
    349                 ? String.format(context.getResources().getQuantityString(
    350                         R.plurals.contacts_count_with_account, memberCount),
    351                                 memberCount, accountWithDataSet.name)
    352                 : context.getResources().getQuantityString(
    353                         R.plurals.contacts_count, memberCount, memberCount);
    354         accountFilterHeader.setText(headerText);
    355         accountFilterHeader.setAllCaps(false);
    356 
    357         // Set icon of the account
    358         final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
    359         final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer
    360                 .findViewById(R.id.account_filter_icon);
    361 
    362         // If it's a writable Google account, we set icon size as 24dp; otherwise, we set it as
    363         // 20dp. And we need to change margin accordingly. This is because the Google icon looks
    364         // smaller when the icons are of the same size.
    365         if (accountType instanceof GoogleAccountType) {
    366             accountFilterHeaderIcon.getLayoutParams().height = getResources()
    367                     .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size);
    368             accountFilterHeaderIcon.getLayoutParams().width = getResources()
    369                     .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size);
    370 
    371             setMargins(accountFilterHeaderIcon,
    372                     getResources().getDimensionPixelOffset(
    373                             R.dimen.contact_browser_list_header_icon_left_margin),
    374                     getResources().getDimensionPixelOffset(
    375                             R.dimen.contact_browser_list_header_icon_right_margin));
    376         } else {
    377             accountFilterHeaderIcon.getLayoutParams().height = getResources()
    378                     .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt);
    379             accountFilterHeaderIcon.getLayoutParams().width = getResources()
    380                     .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt);
    381 
    382             setMargins(accountFilterHeaderIcon,
    383                     getResources().getDimensionPixelOffset(
    384                             R.dimen.contact_browser_list_header_icon_left_margin_alt),
    385                     getResources().getDimensionPixelOffset(
    386                             R.dimen.contact_browser_list_header_icon_right_margin_alt));
    387         }
    388         accountFilterHeaderIcon.requestLayout();
    389 
    390         accountFilterHeaderIcon.setVisibility(View.VISIBLE);
    391         accountFilterHeaderIcon.setImageDrawable(icon);
    392     }
    393 
    394     private boolean shouldShowAccountName(AccountType accountType) {
    395         return (accountType.isGroupMembershipEditable() && this instanceof GroupMembersFragment)
    396                 || GoogleAccountType.ACCOUNT_TYPE.equals(accountType.accountType);
    397     }
    398 
    399     private void setMargins(View v, int l, int r) {
    400         if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
    401             ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
    402             p.setMarginStart(l);
    403             p.setMarginEnd(r);
    404             v.setLayoutParams(p);
    405             v.requestLayout();
    406         }
    407     }
    408 
    409     private void bindListHeaderCommon(View listView, View accountFilterContainer) {
    410         // Show header and remove top padding of the list
    411         accountFilterContainer.setVisibility(View.VISIBLE);
    412         setListViewPaddingTop(listView, /* paddingTop */ 0);
    413     }
    414 
    415     /**
    416      * Hide header of list view and add padding to the top of list view.
    417      */
    418     protected void hideHeaderAndAddPadding(Context context, View listView,
    419             View accountFilterContainer) {
    420         accountFilterContainer.setVisibility(View.GONE);
    421         setListViewPaddingTop(listView,
    422                 /* paddingTop */ context.getResources().getDimensionPixelSize(
    423                         R.dimen.contact_browser_list_item_padding_top_or_bottom));
    424     }
    425 
    426     private void setListViewPaddingTop(View listView, int paddingTop) {
    427         listView.setPadding(listView.getPaddingLeft(), paddingTop, listView.getPaddingRight(),
    428                 listView.getPaddingBottom());
    429     }
    430 
    431     /**
    432      * Returns the {@link ActionBarAdapter} object associated with list fragment.
    433      */
    434     public ActionBarAdapter getActionBarAdapter() {
    435         return null;
    436     }
    437 }
    438