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