Home | History | Annotate | Download | only in contactsfragment
      1 /*
      2  * Copyright (C) 2017 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.dialer.contactsfragment;
     18 
     19 import android.app.Fragment;
     20 import android.app.LoaderManager.LoaderCallbacks;
     21 import android.content.Loader;
     22 import android.content.pm.PackageManager;
     23 import android.database.Cursor;
     24 import android.os.Bundle;
     25 import android.provider.ContactsContract.Contacts;
     26 import android.support.annotation.Nullable;
     27 import android.support.v13.app.FragmentCompat;
     28 import android.support.v7.widget.LinearLayoutManager;
     29 import android.support.v7.widget.RecyclerView;
     30 import android.support.v7.widget.RecyclerView.Recycler;
     31 import android.support.v7.widget.RecyclerView.State;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.View.OnScrollChangeListener;
     35 import android.view.ViewGroup;
     36 import android.widget.TextView;
     37 import com.android.contacts.common.preference.ContactsPreferences;
     38 import com.android.contacts.common.preference.ContactsPreferences.ChangeListener;
     39 import com.android.dialer.common.Assert;
     40 import com.android.dialer.common.LogUtil;
     41 import com.android.dialer.performancereport.PerformanceReport;
     42 import com.android.dialer.util.DialerUtils;
     43 import com.android.dialer.util.IntentUtil;
     44 import com.android.dialer.util.PermissionsUtil;
     45 import com.android.dialer.widget.EmptyContentView;
     46 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
     47 import java.util.Arrays;
     48 
     49 /** Fragment containing a list of all contacts. */
     50 public class ContactsFragment extends Fragment
     51     implements LoaderCallbacks<Cursor>,
     52         OnScrollChangeListener,
     53         OnEmptyViewActionButtonClickedListener,
     54         ChangeListener {
     55 
     56   public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
     57 
     58   private FastScroller fastScroller;
     59   private TextView anchoredHeader;
     60   private RecyclerView recyclerView;
     61   private LinearLayoutManager manager;
     62   private ContactsAdapter adapter;
     63   private EmptyContentView emptyContentView;
     64 
     65   private ContactsPreferences contactsPrefs;
     66 
     67   @Override
     68   public void onCreate(@Nullable Bundle savedInstanceState) {
     69     super.onCreate(savedInstanceState);
     70     contactsPrefs = new ContactsPreferences(getContext());
     71     contactsPrefs.registerChangeListener(this);
     72   }
     73 
     74   @Nullable
     75   @Override
     76   public View onCreateView(
     77       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
     78     View view = inflater.inflate(R.layout.fragment_contacts, container, false);
     79     fastScroller = view.findViewById(R.id.fast_scroller);
     80     anchoredHeader = view.findViewById(R.id.header);
     81     recyclerView = view.findViewById(R.id.recycler_view);
     82 
     83     emptyContentView = view.findViewById(R.id.empty_list_view);
     84     emptyContentView.setImage(R.drawable.empty_contacts);
     85     emptyContentView.setActionClickedListener(this);
     86 
     87     if (PermissionsUtil.hasContactsReadPermissions(getContext())) {
     88       getLoaderManager().initLoader(0, null, this);
     89     } else {
     90       emptyContentView.setDescription(R.string.permission_no_contacts);
     91       emptyContentView.setActionLabel(R.string.permission_single_turn_on);
     92       emptyContentView.setVisibility(View.VISIBLE);
     93     }
     94 
     95     return view;
     96   }
     97 
     98   @Override
     99   public void onChange() {
    100     if (getActivity() != null && isAdded()) {
    101       getLoaderManager().restartLoader(0, null, this);
    102     }
    103   }
    104 
    105   /** @return a loader according to sort order and display order. */
    106   @Override
    107   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    108     boolean sortOrderPrimary =
    109         (contactsPrefs.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY);
    110     boolean displayOrderPrimary =
    111         (contactsPrefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY);
    112 
    113     String sortKey = sortOrderPrimary ? Contacts.SORT_KEY_PRIMARY : Contacts.SORT_KEY_ALTERNATIVE;
    114     return displayOrderPrimary
    115         ? ContactsCursorLoader.createInstanceDisplayNamePrimary(getContext(), sortKey)
    116         : ContactsCursorLoader.createInstanceDisplayNameAlternative(getContext(), sortKey);
    117   }
    118 
    119   @Override
    120   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    121     if (cursor.getCount() == 0) {
    122       emptyContentView.setDescription(R.string.all_contacts_empty);
    123       emptyContentView.setActionLabel(R.string.all_contacts_empty_add_contact_action);
    124       emptyContentView.setVisibility(View.VISIBLE);
    125       recyclerView.setVisibility(View.GONE);
    126     } else {
    127       emptyContentView.setVisibility(View.GONE);
    128       recyclerView.setVisibility(View.VISIBLE);
    129       adapter = new ContactsAdapter(getContext(), cursor);
    130       manager =
    131           new LinearLayoutManager(getContext()) {
    132             @Override
    133             public void onLayoutChildren(Recycler recycler, State state) {
    134               super.onLayoutChildren(recycler, state);
    135               int itemsShown = findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1;
    136               if (adapter.getItemCount() > itemsShown) {
    137                 fastScroller.setVisibility(View.VISIBLE);
    138                 recyclerView.setOnScrollChangeListener(ContactsFragment.this);
    139               } else {
    140                 fastScroller.setVisibility(View.GONE);
    141               }
    142             }
    143           };
    144 
    145       recyclerView.setLayoutManager(manager);
    146       recyclerView.setAdapter(adapter);
    147       PerformanceReport.logOnScrollStateChange(recyclerView);
    148       fastScroller.setup(adapter, manager);
    149     }
    150   }
    151 
    152   @Override
    153   public void onLoaderReset(Loader<Cursor> loader) {
    154     recyclerView.setAdapter(null);
    155     recyclerView.setOnScrollChangeListener(null);
    156     adapter = null;
    157     contactsPrefs.unregisterChangeListener();
    158   }
    159 
    160   /*
    161    * When our recycler view updates, we need to ensure that our row headers and anchored header
    162    * are in the correct state.
    163    *
    164    * The general rule is, when the row headers are shown, our anchored header is hidden. When the
    165    * recycler view is scrolling through a sublist that has more than one element, we want to show
    166    * out anchored header, to create the illusion that our row header has been anchored. In all
    167    * other situations, we want to hide the anchor because that means we are transitioning between
    168    * two sublists.
    169    */
    170   @Override
    171   public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
    172     fastScroller.updateContainerAndScrollBarPosition(recyclerView);
    173     int firstVisibleItem = manager.findFirstVisibleItemPosition();
    174     int firstCompletelyVisible = manager.findFirstCompletelyVisibleItemPosition();
    175     if (firstCompletelyVisible == RecyclerView.NO_POSITION) {
    176       // No items are visible, so there are no headers to update.
    177       return;
    178     }
    179     String anchoredHeaderString = adapter.getHeaderString(firstCompletelyVisible);
    180 
    181     // If the user swipes to the top of the list very quickly, there is some strange behavior
    182     // between this method updating headers and adapter#onBindViewHolder updating headers.
    183     // To overcome this, we refresh the headers to ensure they are correct.
    184     if (firstVisibleItem == firstCompletelyVisible && firstVisibleItem == 0) {
    185       adapter.refreshHeaders();
    186       anchoredHeader.setVisibility(View.INVISIBLE);
    187     } else if (firstVisibleItem != 0) { // skip the add contact row
    188       if (adapter.getHeaderString(firstVisibleItem).equals(anchoredHeaderString)) {
    189         anchoredHeader.setText(anchoredHeaderString);
    190         anchoredHeader.setVisibility(View.VISIBLE);
    191         getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.INVISIBLE);
    192         getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.INVISIBLE);
    193       } else {
    194         anchoredHeader.setVisibility(View.INVISIBLE);
    195         getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.VISIBLE);
    196         getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.VISIBLE);
    197       }
    198     }
    199   }
    200 
    201   private ContactViewHolder getContactHolder(int position) {
    202     return ((ContactViewHolder) recyclerView.findViewHolderForAdapterPosition(position));
    203   }
    204 
    205   @Override
    206   public void onEmptyViewActionButtonClicked() {
    207     if (emptyContentView.getActionLabel() == R.string.permission_single_turn_on) {
    208       String[] deniedPermissions =
    209           PermissionsUtil.getPermissionsCurrentlyDenied(
    210               getContext(), PermissionsUtil.allContactsGroupPermissionsUsedInDialer);
    211       if (deniedPermissions.length > 0) {
    212         LogUtil.i(
    213             "ContactsFragment.onEmptyViewActionButtonClicked",
    214             "Requesting permissions: " + Arrays.toString(deniedPermissions));
    215         FragmentCompat.requestPermissions(
    216             this, deniedPermissions, READ_CONTACTS_PERMISSION_REQUEST_CODE);
    217       }
    218 
    219     } else if (emptyContentView.getActionLabel()
    220         == R.string.all_contacts_empty_add_contact_action) {
    221       // Add new contact
    222       DialerUtils.startActivityWithErrorToast(
    223           getContext(), IntentUtil.getNewContactIntent(), R.string.add_contact_not_available);
    224     } else {
    225       throw Assert.createIllegalStateFailException("Invalid empty content view action label.");
    226     }
    227   }
    228 
    229   @Override
    230   public void onRequestPermissionsResult(
    231       int requestCode, String[] permissions, int[] grantResults) {
    232     if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
    233       if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
    234         // Force a refresh of the data since we were missing the permission before this.
    235         emptyContentView.setVisibility(View.GONE);
    236         getLoaderManager().initLoader(0, null, this);
    237       }
    238     }
    239   }
    240 }
    241