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