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