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 static android.Manifest.permission.READ_CONTACTS; 20 21 import android.app.Fragment; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.content.pm.PackageManager; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.support.annotation.IntDef; 32 import android.support.annotation.Nullable; 33 import android.support.v13.app.FragmentCompat; 34 import android.support.v7.widget.LinearLayoutManager; 35 import android.support.v7.widget.RecyclerView; 36 import android.support.v7.widget.RecyclerView.Recycler; 37 import android.support.v7.widget.RecyclerView.State; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.View.OnScrollChangeListener; 41 import android.view.ViewGroup; 42 import android.widget.ImageView; 43 import android.widget.TextView; 44 import com.android.dialer.common.Assert; 45 import com.android.dialer.common.FragmentUtils; 46 import com.android.dialer.common.LogUtil; 47 import com.android.dialer.performancereport.PerformanceReport; 48 import com.android.dialer.util.DialerUtils; 49 import com.android.dialer.util.IntentUtil; 50 import com.android.dialer.util.PermissionsUtil; 51 import com.android.dialer.widget.EmptyContentView; 52 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; 53 import java.lang.annotation.Retention; 54 import java.lang.annotation.RetentionPolicy; 55 import java.util.Arrays; 56 57 /** Fragment containing a list of all contacts. */ 58 public class ContactsFragment extends Fragment 59 implements LoaderCallbacks<Cursor>, 60 OnScrollChangeListener, 61 OnEmptyViewActionButtonClickedListener { 62 63 /** An enum for the different types of headers that be inserted at position 0 in the list. */ 64 @Retention(RetentionPolicy.SOURCE) 65 @IntDef({Header.NONE, Header.ADD_CONTACT}) 66 public @interface Header { 67 int NONE = 0; 68 /** Header that allows the user to add a new contact. */ 69 int ADD_CONTACT = 1; 70 } 71 72 public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; 73 74 private static final String EXTRA_HEADER = "extra_header"; 75 private static final String EXTRA_HAS_PHONE_NUMBERS = "extra_has_phone_numbers"; 76 77 /** 78 * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS 79 * permission is granted via the UI in another fragment. 80 */ 81 private final BroadcastReceiver readContactsPermissionGrantedReceiver = 82 new BroadcastReceiver() { 83 @Override 84 public void onReceive(Context context, Intent intent) { 85 loadContacts(); 86 } 87 }; 88 89 private FastScroller fastScroller; 90 private TextView anchoredHeader; 91 private RecyclerView recyclerView; 92 private LinearLayoutManager manager; 93 private ContactsAdapter adapter; 94 private EmptyContentView emptyContentView; 95 96 private @Header int header; 97 98 private boolean hasPhoneNumbers; 99 private String query; 100 101 /** 102 * Used to get a configured instance of ContactsFragment. 103 * 104 * <p>Current example of this fragment are the contacts tab and in creating a new favorite 105 * contact. For example, the contacts tab we use: 106 * 107 * <ul> 108 * <li>{@link Header#ADD_CONTACT} to insert a header that allows users to add a contact 109 * <li>Open contact cards on click 110 * </ul> 111 * 112 * And for the add favorite contact screen we might use: 113 * 114 * <ul> 115 * <li>{@link Header#NONE} so that all rows are contacts (i.e. no header inserted) 116 * <li>Send a selected contact to the parent activity. 117 * </ul> 118 * 119 * @param header determines the type of header inserted at position 0 in the contacts list 120 */ newInstance(@eader int header)121 public static ContactsFragment newInstance(@Header int header) { 122 ContactsFragment fragment = new ContactsFragment(); 123 Bundle args = new Bundle(); 124 args.putInt(EXTRA_HEADER, header); 125 fragment.setArguments(args); 126 return fragment; 127 } 128 129 /** 130 * Returns {@link ContactsFragment} with a list of contacts such that: 131 * 132 * <ul> 133 * <li>Each contact has a phone number 134 * <li>Contacts are filterable via {@link #updateQuery(String)} 135 * <li>There is no list header (i.e. {@link Header#NONE} 136 * <li>Clicking on a contact notifies the parent activity via {@link 137 * OnContactSelectedListener#onContactSelected(ImageView, Uri, long)}. 138 * </ul> 139 */ newAddFavoritesInstance()140 public static ContactsFragment newAddFavoritesInstance() { 141 ContactsFragment fragment = new ContactsFragment(); 142 Bundle args = new Bundle(); 143 args.putInt(EXTRA_HEADER, Header.NONE); 144 args.putBoolean(EXTRA_HAS_PHONE_NUMBERS, true); 145 fragment.setArguments(args); 146 return fragment; 147 } 148 149 @SuppressWarnings("WrongConstant") 150 @Override onCreate(@ullable Bundle savedInstanceState)151 public void onCreate(@Nullable Bundle savedInstanceState) { 152 super.onCreate(savedInstanceState); 153 header = getArguments().getInt(EXTRA_HEADER); 154 hasPhoneNumbers = getArguments().getBoolean(EXTRA_HAS_PHONE_NUMBERS); 155 if (savedInstanceState == null) { 156 // The onHiddenChanged callback does not get called the first time the fragment is 157 // attached, so call it ourselves here. 158 onHiddenChanged(false); 159 } 160 } 161 162 @Override onStart()163 public void onStart() { 164 super.onStart(); 165 PermissionsUtil.registerPermissionReceiver( 166 getActivity(), readContactsPermissionGrantedReceiver, READ_CONTACTS); 167 } 168 169 @Override onStop()170 public void onStop() { 171 PermissionsUtil.unregisterPermissionReceiver( 172 getActivity(), readContactsPermissionGrantedReceiver); 173 super.onStop(); 174 } 175 176 @Nullable 177 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)178 public View onCreateView( 179 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 180 View view = inflater.inflate(R.layout.fragment_contacts, container, false); 181 fastScroller = view.findViewById(R.id.fast_scroller); 182 anchoredHeader = view.findViewById(R.id.header); 183 recyclerView = view.findViewById(R.id.recycler_view); 184 adapter = 185 new ContactsAdapter( 186 getContext(), header, FragmentUtils.getParent(this, OnContactSelectedListener.class)); 187 recyclerView.setAdapter(adapter); 188 manager = 189 new LinearLayoutManager(getContext()) { 190 @Override 191 public void onLayoutChildren(Recycler recycler, State state) { 192 super.onLayoutChildren(recycler, state); 193 int itemsShown = findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1; 194 if (adapter.getItemCount() > itemsShown) { 195 fastScroller.setVisibility(View.VISIBLE); 196 recyclerView.setOnScrollChangeListener(ContactsFragment.this); 197 } else { 198 fastScroller.setVisibility(View.GONE); 199 } 200 } 201 }; 202 recyclerView.setLayoutManager(manager); 203 204 emptyContentView = view.findViewById(R.id.empty_list_view); 205 emptyContentView.setImage(R.drawable.empty_contacts); 206 emptyContentView.setActionClickedListener(this); 207 208 if (PermissionsUtil.hasContactsReadPermissions(getContext())) { 209 loadContacts(); 210 } else { 211 emptyContentView.setDescription(R.string.permission_no_contacts); 212 emptyContentView.setActionLabel(R.string.permission_single_turn_on); 213 emptyContentView.setVisibility(View.VISIBLE); 214 recyclerView.setVisibility(View.GONE); 215 } 216 217 return view; 218 } 219 220 @Override onResume()221 public void onResume() { 222 super.onResume(); 223 if (getActivity() != null 224 && isAdded() 225 && PermissionsUtil.hasContactsReadPermissions(getContext())) { 226 getLoaderManager().restartLoader(0, null, this); 227 } 228 } 229 230 /** @return a loader according to sort order and display order. */ 231 @Override onCreateLoader(int id, Bundle args)232 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 233 ContactsCursorLoader cursorLoader = new ContactsCursorLoader(getContext(), hasPhoneNumbers); 234 cursorLoader.setQuery(query); 235 return cursorLoader; 236 } 237 updateQuery(String query)238 public void updateQuery(String query) { 239 this.query = query; 240 getLoaderManager().restartLoader(0, null, this); 241 } 242 243 @Override onLoadFinished(Loader<Cursor> loader, Cursor cursor)244 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 245 LogUtil.enterBlock("ContactsFragment.onLoadFinished"); 246 if (cursor == null || cursor.getCount() == 0) { 247 emptyContentView.setDescription(R.string.all_contacts_empty); 248 emptyContentView.setActionLabel(R.string.all_contacts_empty_add_contact_action); 249 emptyContentView.setVisibility(View.VISIBLE); 250 recyclerView.setVisibility(View.GONE); 251 } else { 252 emptyContentView.setVisibility(View.GONE); 253 recyclerView.setVisibility(View.VISIBLE); 254 adapter.updateCursor(cursor); 255 256 PerformanceReport.logOnScrollStateChange(recyclerView); 257 fastScroller.setup(adapter, manager); 258 } 259 } 260 261 @Override onLoaderReset(Loader<Cursor> loader)262 public void onLoaderReset(Loader<Cursor> loader) { 263 recyclerView.setAdapter(null); 264 recyclerView.setOnScrollChangeListener(null); 265 adapter = null; 266 } 267 268 /* 269 * When our recycler view updates, we need to ensure that our row headers and anchored header 270 * are in the correct state. 271 * 272 * The general rule is, when the row headers are shown, our anchored header is hidden. When the 273 * recycler view is scrolling through a sublist that has more than one element, we want to show 274 * out anchored header, to create the illusion that our row header has been anchored. In all 275 * other situations, we want to hide the anchor because that means we are transitioning between 276 * two sublists. 277 */ 278 @Override onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)279 public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { 280 fastScroller.updateContainerAndScrollBarPosition(recyclerView); 281 int firstVisibleItem = manager.findFirstVisibleItemPosition(); 282 int firstCompletelyVisible = manager.findFirstCompletelyVisibleItemPosition(); 283 if (firstCompletelyVisible == RecyclerView.NO_POSITION) { 284 // No items are visible, so there are no headers to update. 285 return; 286 } 287 String anchoredHeaderString = adapter.getHeaderString(firstCompletelyVisible); 288 289 OnContactsListScrolledListener listener = 290 FragmentUtils.getParent(this, OnContactsListScrolledListener.class); 291 if (listener != null) { 292 listener.onContactsListScrolled( 293 recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING 294 || fastScroller.isDragStarted()); 295 } 296 297 // If the user swipes to the top of the list very quickly, there is some strange behavior 298 // between this method updating headers and adapter#onBindViewHolder updating headers. 299 // To overcome this, we refresh the headers to ensure they are correct. 300 if (firstVisibleItem == firstCompletelyVisible && firstVisibleItem == 0) { 301 adapter.refreshHeaders(); 302 anchoredHeader.setVisibility(View.INVISIBLE); 303 } else if (firstVisibleItem != 0) { // skip the add contact row 304 if (adapter.getHeaderString(firstVisibleItem).equals(anchoredHeaderString)) { 305 anchoredHeader.setText(anchoredHeaderString); 306 anchoredHeader.setVisibility(View.VISIBLE); 307 getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.INVISIBLE); 308 getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.INVISIBLE); 309 } else { 310 anchoredHeader.setVisibility(View.INVISIBLE); 311 getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.VISIBLE); 312 getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.VISIBLE); 313 } 314 } 315 } 316 getContactHolder(int position)317 private ContactViewHolder getContactHolder(int position) { 318 return ((ContactViewHolder) recyclerView.findViewHolderForAdapterPosition(position)); 319 } 320 321 @Override onEmptyViewActionButtonClicked()322 public void onEmptyViewActionButtonClicked() { 323 if (emptyContentView.getActionLabel() == R.string.permission_single_turn_on) { 324 String[] deniedPermissions = 325 PermissionsUtil.getPermissionsCurrentlyDenied( 326 getContext(), PermissionsUtil.allContactsGroupPermissionsUsedInDialer); 327 if (deniedPermissions.length > 0) { 328 LogUtil.i( 329 "ContactsFragment.onEmptyViewActionButtonClicked", 330 "Requesting permissions: " + Arrays.toString(deniedPermissions)); 331 FragmentCompat.requestPermissions( 332 this, deniedPermissions, READ_CONTACTS_PERMISSION_REQUEST_CODE); 333 } 334 335 } else if (emptyContentView.getActionLabel() 336 == R.string.all_contacts_empty_add_contact_action) { 337 // Add new contact 338 DialerUtils.startActivityWithErrorToast( 339 getContext(), IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); 340 } else { 341 throw Assert.createIllegalStateFailException("Invalid empty content view action label."); 342 } 343 } 344 345 @Override onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)346 public void onRequestPermissionsResult( 347 int requestCode, String[] permissions, int[] grantResults) { 348 if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { 349 if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { 350 // Force a refresh of the data since we were missing the permission before this. 351 PermissionsUtil.notifyPermissionGranted(getContext(), permissions[0]); 352 } 353 } 354 } 355 356 @Override onHiddenChanged(boolean hidden)357 public void onHiddenChanged(boolean hidden) { 358 super.onHiddenChanged(hidden); 359 OnContactsFragmentHiddenChangedListener listener = 360 FragmentUtils.getParent(this, OnContactsFragmentHiddenChangedListener.class); 361 if (listener != null) { 362 listener.onContactsFragmentHiddenChanged(hidden); 363 } 364 } 365 loadContacts()366 private void loadContacts() { 367 getLoaderManager().initLoader(0, null, this); 368 recyclerView.setVisibility(View.VISIBLE); 369 emptyContentView.setVisibility(View.GONE); 370 } 371 372 /** Listener for contacts list scroll state. */ 373 public interface OnContactsListScrolledListener { onContactsListScrolled(boolean isDragging)374 void onContactsListScrolled(boolean isDragging); 375 } 376 377 /** Listener to notify parents when a contact is selected. */ 378 public interface OnContactSelectedListener { 379 380 /** Called when a contact is selected in {@link ContactsFragment}. */ onContactSelected(ImageView photo, Uri contactUri, long contactId)381 void onContactSelected(ImageView photo, Uri contactUri, long contactId); 382 } 383 384 /** Listener for contacts fragment hidden state */ 385 public interface OnContactsFragmentHiddenChangedListener { onContactsFragmentHiddenChanged(boolean hidden)386 void onContactsFragmentHiddenChanged(boolean hidden); 387 } 388 } 389