| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * Copyright (C) 2023 The LineageOS Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.dialer.contactsfragment; |
| |
| import static android.Manifest.permission.READ_CONTACTS; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnScrollChangeListener; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.activity.result.ActivityResultLauncher; |
| import androidx.activity.result.contract.ActivityResultContracts; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.Fragment; |
| import androidx.loader.app.LoaderManager; |
| import androidx.loader.content.Loader; |
| import androidx.recyclerview.widget.LinearLayoutManager; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.dialer.R; |
| import com.android.dialer.common.Assert; |
| import com.android.dialer.common.FragmentUtils; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.util.DialerUtils; |
| import com.android.dialer.util.IntentUtil; |
| import com.android.dialer.util.PermissionsUtil; |
| import com.android.dialer.widget.EmptyContentView; |
| import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Arrays; |
| |
| /** Fragment containing a list of all contacts. */ |
| public class ContactsFragment extends Fragment |
| implements LoaderManager.LoaderCallbacks<Cursor>, |
| OnScrollChangeListener, |
| OnEmptyViewActionButtonClickedListener { |
| |
| /** An enum for the different types of headers that be inserted at position 0 in the list. */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({Header.NONE, Header.ADD_CONTACT}) |
| public @interface Header { |
| int NONE = 0; |
| /** Header that allows the user to add a new contact. */ |
| int ADD_CONTACT = 1; |
| } |
| |
| private static final String EXTRA_HEADER = "extra_header"; |
| private static final String EXTRA_HAS_PHONE_NUMBERS = "extra_has_phone_numbers"; |
| |
| /** |
| * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS |
| * permission is granted via the UI in another fragment. |
| */ |
| private final BroadcastReceiver readContactsPermissionGrantedReceiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| loadContacts(); |
| } |
| }; |
| |
| private final ActivityResultLauncher<String[]> permissionLauncher = registerForActivityResult( |
| new ActivityResultContracts.RequestMultiplePermissions(), |
| grantResults -> { |
| if (grantResults.size() >= 1 && grantResults.values().iterator().next()) { |
| String key = grantResults.keySet().iterator().next(); |
| // Force a refresh of the data since we were missing the permission before this. |
| PermissionsUtil.notifyPermissionGranted(getContext(), key); |
| } |
| }); |
| |
| private FastScroller fastScroller; |
| private TextView anchoredHeader; |
| private RecyclerView recyclerView; |
| private LinearLayoutManager manager; |
| private ContactsAdapter adapter; |
| private EmptyContentView emptyContentView; |
| |
| private @Header int header; |
| |
| private boolean hasPhoneNumbers; |
| private String query; |
| |
| /** |
| * Used to get a configured instance of ContactsFragment. |
| * |
| * <p>Current example of this fragment are the contacts tab and in creating a new favorite |
| * contact. For example, the contacts tab we use: |
| * |
| * <ul> |
| * <li>{@link Header#ADD_CONTACT} to insert a header that allows users to add a contact |
| * <li>Open contact cards on click |
| * </ul> |
| * |
| * And for the add favorite contact screen we might use: |
| * |
| * <ul> |
| * <li>{@link Header#NONE} so that all rows are contacts (i.e. no header inserted) |
| * <li>Send a selected contact to the parent activity. |
| * </ul> |
| * |
| * @param header determines the type of header inserted at position 0 in the contacts list |
| */ |
| public static ContactsFragment newInstance(@Header int header) { |
| ContactsFragment fragment = new ContactsFragment(); |
| Bundle args = new Bundle(); |
| args.putInt(EXTRA_HEADER, header); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @SuppressWarnings("WrongConstant") |
| @Override |
| public void onCreate(@Nullable Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| header = getArguments().getInt(EXTRA_HEADER); |
| hasPhoneNumbers = getArguments().getBoolean(EXTRA_HAS_PHONE_NUMBERS); |
| if (savedInstanceState == null) { |
| // The onHiddenChanged callback does not get called the first time the fragment is |
| // attached, so call it ourselves here. |
| onHiddenChanged(false); |
| } |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| PermissionsUtil.registerPermissionReceiver( |
| getActivity(), readContactsPermissionGrantedReceiver, READ_CONTACTS); |
| } |
| |
| @Override |
| public void onStop() { |
| PermissionsUtil.unregisterPermissionReceiver( |
| getActivity(), readContactsPermissionGrantedReceiver); |
| super.onStop(); |
| } |
| |
| @Nullable |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| View view = inflater.inflate(R.layout.fragment_contacts, container, false); |
| fastScroller = view.findViewById(R.id.fast_scroller); |
| anchoredHeader = view.findViewById(R.id.header); |
| recyclerView = view.findViewById(R.id.recycler_view); |
| adapter = |
| new ContactsAdapter( |
| getContext(), header, FragmentUtils.getParent(this, OnContactSelectedListener.class)); |
| recyclerView.setAdapter(adapter); |
| manager = |
| new LinearLayoutManager(getContext()) { |
| @Override |
| public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { |
| super.onLayoutChildren(recycler, state); |
| int itemsShown = findLastVisibleItemPosition() - findFirstVisibleItemPosition() + 1; |
| if (adapter.getItemCount() > itemsShown) { |
| fastScroller.setVisibility(View.VISIBLE); |
| recyclerView.setOnScrollChangeListener(ContactsFragment.this); |
| } else { |
| fastScroller.setVisibility(View.GONE); |
| } |
| } |
| }; |
| recyclerView.setLayoutManager(manager); |
| |
| emptyContentView = view.findViewById(R.id.empty_list_view); |
| emptyContentView.setImage(R.drawable.empty_contacts); |
| emptyContentView.setActionClickedListener(this); |
| |
| if (PermissionsUtil.hasContactsReadPermissions(getContext())) { |
| loadContacts(); |
| } else { |
| emptyContentView.setDescription(R.string.permission_no_contacts); |
| emptyContentView.setActionLabel(R.string.permission_single_turn_on); |
| emptyContentView.setVisibility(View.VISIBLE); |
| recyclerView.setVisibility(View.GONE); |
| } |
| |
| return view; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| if (getActivity() != null |
| && isAdded() |
| && PermissionsUtil.hasContactsReadPermissions(getContext())) { |
| LoaderManager.getInstance(this).restartLoader(0, null, this); |
| } |
| } |
| |
| /** @return a loader according to sort order and display order. */ |
| @Override |
| public Loader<Cursor> onCreateLoader(int id, Bundle args) { |
| ContactsCursorLoader cursorLoader = new ContactsCursorLoader(getContext(), hasPhoneNumbers); |
| cursorLoader.setQuery(query); |
| return cursorLoader; |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { |
| LogUtil.enterBlock("ContactsFragment.onLoadFinished"); |
| if (cursor == null || cursor.getCount() == 0) { |
| emptyContentView.setDescription(R.string.all_contacts_empty); |
| emptyContentView.setActionLabel(R.string.all_contacts_empty_add_contact_action); |
| emptyContentView.setVisibility(View.VISIBLE); |
| recyclerView.setVisibility(View.GONE); |
| } else { |
| emptyContentView.setVisibility(View.GONE); |
| recyclerView.setVisibility(View.VISIBLE); |
| adapter.updateCursor(cursor); |
| |
| fastScroller.setup(adapter, manager); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Cursor> loader) { |
| recyclerView.setAdapter(null); |
| recyclerView.setOnScrollChangeListener(null); |
| adapter = null; |
| } |
| |
| /* |
| * When our recycler view updates, we need to ensure that our row headers and anchored header |
| * are in the correct state. |
| * |
| * The general rule is, when the row headers are shown, our anchored header is hidden. When the |
| * recycler view is scrolling through a sublist that has more than one element, we want to show |
| * out anchored header, to create the illusion that our row header has been anchored. In all |
| * other situations, we want to hide the anchor because that means we are transitioning between |
| * two sublists. |
| */ |
| @Override |
| public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { |
| fastScroller.updateContainerAndScrollBarPosition(recyclerView); |
| int firstVisibleItem = manager.findFirstVisibleItemPosition(); |
| int firstCompletelyVisible = manager.findFirstCompletelyVisibleItemPosition(); |
| if (firstCompletelyVisible == RecyclerView.NO_POSITION) { |
| // No items are visible, so there are no headers to update. |
| return; |
| } |
| String anchoredHeaderString = adapter.getHeaderString(firstCompletelyVisible); |
| |
| OnContactsListScrolledListener listener = |
| FragmentUtils.getParent(this, OnContactsListScrolledListener.class); |
| if (listener != null) { |
| listener.onContactsListScrolled( |
| recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING |
| || fastScroller.isDragStarted()); |
| } |
| |
| // If the user swipes to the top of the list very quickly, there is some strange behavior |
| // between this method updating headers and adapter#onBindViewHolder updating headers. |
| // To overcome this, we refresh the headers to ensure they are correct. |
| if (firstVisibleItem == firstCompletelyVisible && firstVisibleItem == 0) { |
| adapter.refreshHeaders(); |
| anchoredHeader.setVisibility(View.INVISIBLE); |
| } else if (firstVisibleItem != 0) { // skip the add contact row |
| if (adapter.getHeaderString(firstVisibleItem).equals(anchoredHeaderString)) { |
| anchoredHeader.setText(anchoredHeaderString); |
| anchoredHeader.setVisibility(View.VISIBLE); |
| getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.INVISIBLE); |
| getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.INVISIBLE); |
| } else { |
| anchoredHeader.setVisibility(View.INVISIBLE); |
| getContactHolder(firstVisibleItem).getHeaderView().setVisibility(View.VISIBLE); |
| getContactHolder(firstCompletelyVisible).getHeaderView().setVisibility(View.VISIBLE); |
| } |
| } |
| } |
| |
| private ContactViewHolder getContactHolder(int position) { |
| return ((ContactViewHolder) recyclerView.findViewHolderForAdapterPosition(position)); |
| } |
| |
| @Override |
| public void onEmptyViewActionButtonClicked() { |
| if (emptyContentView.getActionLabel() == R.string.permission_single_turn_on) { |
| String[] deniedPermissions = |
| PermissionsUtil.getPermissionsCurrentlyDenied( |
| getContext(), PermissionsUtil.allContactsGroupPermissionsUsedInDialer); |
| if (deniedPermissions.length > 0) { |
| LogUtil.i( |
| "ContactsFragment.onEmptyViewActionButtonClicked", |
| "Requesting permissions: " + Arrays.toString(deniedPermissions)); |
| permissionLauncher.launch(deniedPermissions); |
| } |
| |
| } else if (emptyContentView.getActionLabel() |
| == R.string.all_contacts_empty_add_contact_action) { |
| // Add new contact |
| DialerUtils.startActivityWithErrorToast( |
| getContext(), IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); |
| } else { |
| throw Assert.createIllegalStateFailException("Invalid empty content view action label."); |
| } |
| } |
| |
| @Override |
| public void onHiddenChanged(boolean hidden) { |
| super.onHiddenChanged(hidden); |
| OnContactsFragmentHiddenChangedListener listener = |
| FragmentUtils.getParent(this, OnContactsFragmentHiddenChangedListener.class); |
| if (listener != null) { |
| listener.onContactsFragmentHiddenChanged(hidden); |
| } |
| } |
| |
| private void loadContacts() { |
| LoaderManager.getInstance(this).initLoader(0, null, this); |
| recyclerView.setVisibility(View.VISIBLE); |
| emptyContentView.setVisibility(View.GONE); |
| } |
| |
| /** Listener for contacts list scroll state. */ |
| public interface OnContactsListScrolledListener { |
| void onContactsListScrolled(boolean isDragging); |
| } |
| |
| /** Listener to notify parents when a contact is selected. */ |
| public interface OnContactSelectedListener { |
| |
| /** Called when a contact is selected in {@link ContactsFragment}. */ |
| void onContactSelected(ImageView photo, Uri contactUri, long contactId); |
| } |
| |
| /** Listener for contacts fragment hidden state */ |
| public interface OnContactsFragmentHiddenChangedListener { |
| void onContactsFragmentHiddenChanged(boolean hidden); |
| } |
| } |