summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/contactsfragment
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/contactsfragment')
-rw-r--r--java/com/android/dialer/contactsfragment/AddContactViewHolder.java42
-rw-r--r--java/com/android/dialer/contactsfragment/ContactViewHolder.java38
-rw-r--r--java/com/android/dialer/contactsfragment/ContactsAdapter.java123
-rw-r--r--java/com/android/dialer/contactsfragment/ContactsCursorLoader.java29
-rw-r--r--java/com/android/dialer/contactsfragment/ContactsFragment.java227
-rw-r--r--java/com/android/dialer/contactsfragment/FastScroller.java131
-rw-r--r--java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_container_background.xml28
-rw-r--r--java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_scroll_bar.xml32
-rw-r--r--java/com/android/dialer/contactsfragment/res/layout/add_contact_row.xml50
-rw-r--r--java/com/android/dialer/contactsfragment/res/layout/contact_row.xml6
-rw-r--r--java/com/android/dialer/contactsfragment/res/layout/fragment_contacts.xml41
-rw-r--r--java/com/android/dialer/contactsfragment/res/values/dimens.xml9
12 files changed, 682 insertions, 74 deletions
diff --git a/java/com/android/dialer/contactsfragment/AddContactViewHolder.java b/java/com/android/dialer/contactsfragment/AddContactViewHolder.java
new file mode 100644
index 000000000..09c222e45
--- /dev/null
+++ b/java/com/android/dialer/contactsfragment/AddContactViewHolder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 The Android Open Source 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 android.content.Context;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+
+/** ViewHolder for {@link ContactsFragment} to display add contact row. */
+final class AddContactViewHolder extends ViewHolder implements OnClickListener {
+
+ private final Context context;
+
+ AddContactViewHolder(View view) {
+ super(view);
+ view.setOnClickListener(this);
+ context = view.getContext();
+ }
+
+ @Override
+ public void onClick(View v) {
+ DialerUtils.startActivityWithErrorToast(
+ context, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available);
+ }
+}
diff --git a/java/com/android/dialer/contactsfragment/ContactViewHolder.java b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
index 5df106dbc..0597c2a7e 100644
--- a/java/com/android/dialer/contactsfragment/ContactViewHolder.java
+++ b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
@@ -16,6 +16,7 @@
package com.android.dialer.contactsfragment;
+import android.content.Context;
import android.net.Uri;
import android.provider.ContactsContract.QuickContact;
import android.support.v7.widget.RecyclerView;
@@ -25,6 +26,9 @@ import android.view.View.OnClickListener;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import com.android.dialer.common.Assert;
+import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
+import com.android.dialer.logging.InteractionEvent;
+import com.android.dialer.logging.Logger;
/** View holder for a contact. */
final class ContactViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
@@ -32,16 +36,21 @@ final class ContactViewHolder extends RecyclerView.ViewHolder implements OnClick
private final TextView header;
private final TextView name;
private final QuickContactBadge photo;
+ private final Context context;
+ private final @ClickAction int clickAction;
private String headerText;
private Uri contactUri;
- public ContactViewHolder(View itemView) {
+ ContactViewHolder(View itemView, @ClickAction int clickAction) {
super(itemView);
+ Assert.checkArgument(clickAction != ClickAction.INVALID, "Invalid click action.");
+ context = itemView.getContext();
itemView.findViewById(R.id.click_target).setOnClickListener(this);
- header = (TextView) itemView.findViewById(R.id.header);
- name = (TextView) itemView.findViewById(R.id.contact_name);
- photo = (QuickContactBadge) itemView.findViewById(R.id.photo);
+ header = itemView.findViewById(R.id.header);
+ name = itemView.findViewById(R.id.contact_name);
+ photo = itemView.findViewById(R.id.photo);
+ this.clickAction = clickAction;
}
/**
@@ -60,6 +69,10 @@ final class ContactViewHolder extends RecyclerView.ViewHolder implements OnClick
name.setText(displayName);
header.setText(headerText);
header.setVisibility(showHeader ? View.VISIBLE : View.INVISIBLE);
+
+ Logger.get(context)
+ .logQuickContactOnTouch(
+ photo, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CONTACTS_FRAGMENT_BADGE, true);
}
public QuickContactBadge getPhoto() {
@@ -76,7 +89,20 @@ final class ContactViewHolder extends RecyclerView.ViewHolder implements OnClick
@Override
public void onClick(View v) {
- QuickContact.showQuickContact(
- photo.getContext(), photo, contactUri, QuickContact.MODE_LARGE, null /* excludeMimes */);
+ switch (clickAction) {
+ case ClickAction.OPEN_CONTACT_CARD:
+ Logger.get(context)
+ .logInteraction(InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CONTACTS_FRAGMENT_ITEM);
+ QuickContact.showQuickContact(
+ photo.getContext(),
+ photo,
+ contactUri,
+ QuickContact.MODE_LARGE,
+ null /* excludeMimes */);
+ break;
+ case ClickAction.INVALID:
+ default:
+ throw Assert.createIllegalStateFailException("Invalid click action.");
+ }
}
}
diff --git a/java/com/android/dialer/contactsfragment/ContactsAdapter.java b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
index 4692eff5d..13895313f 100644
--- a/java/com/android/dialer/contactsfragment/ContactsAdapter.java
+++ b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
@@ -20,20 +20,37 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
+import android.support.annotation.IntDef;
import android.support.v4.util.ArrayMap;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
-import android.widget.TextView;
-import com.android.contacts.common.ContactPhotoManager;
import com.android.dialer.common.Assert;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
+import com.android.dialer.contactsfragment.ContactsFragment.Header;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/** List adapter for the union of all contacts associated with every account on the device. */
-final class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
+final class ContactsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final int UNKNOWN_VIEW_TYPE = 0;
+ private static final int ADD_CONTACT_VIEW_TYPE = 1;
+ private static final int CONTACT_VIEW_TYPE = 2;
+
+ /** An Enum for the different row view types shown by this adapter. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNKNOWN_VIEW_TYPE, ADD_CONTACT_VIEW_TYPE, CONTACT_VIEW_TYPE})
+ @interface ContactsViewType {}
private final ArrayMap<ContactViewHolder, Integer> holderMap = new ArrayMap<>();
private final Context context;
private final Cursor cursor;
+ private final @Header int header;
+ private final @ClickAction int clickAction;
// List of contact sublist headers
private final String[] headers;
@@ -41,23 +58,44 @@ final class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
// Number of contacts that correspond to each header in {@code headers}.
private final int[] counts;
- public ContactsAdapter(Context context, Cursor cursor) {
+ ContactsAdapter(
+ Context context, Cursor cursor, @Header int header, @ClickAction int clickAction) {
this.context = context;
this.cursor = cursor;
+ this.header = header;
+ this.clickAction = clickAction;
headers = cursor.getExtras().getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
counts = cursor.getExtras().getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
}
@Override
- public ContactViewHolder onCreateViewHolder(ViewGroup parent, int position) {
- return new ContactViewHolder(
- LayoutInflater.from(context).inflate(R.layout.contact_row, parent, false));
+ public RecyclerView.ViewHolder onCreateViewHolder(
+ ViewGroup parent, @ContactsViewType int viewType) {
+ switch (viewType) {
+ case ADD_CONTACT_VIEW_TYPE:
+ return new AddContactViewHolder(
+ LayoutInflater.from(context).inflate(R.layout.add_contact_row, parent, false));
+ case CONTACT_VIEW_TYPE:
+ return new ContactViewHolder(
+ LayoutInflater.from(context).inflate(R.layout.contact_row, parent, false), clickAction);
+ case UNKNOWN_VIEW_TYPE:
+ default:
+ throw Assert.createIllegalStateFailException("Invalid view type: " + viewType);
+ }
}
@Override
- public void onBindViewHolder(ContactViewHolder contactViewHolder, int position) {
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ if (viewHolder instanceof AddContactViewHolder) {
+ return;
+ }
+
+ ContactViewHolder contactViewHolder = (ContactViewHolder) viewHolder;
holderMap.put(contactViewHolder, position);
cursor.moveToPosition(position);
+ if (header != Header.NONE) {
+ cursor.moveToPrevious();
+ }
String name = getDisplayName(cursor);
String header = getHeaderString(position);
@@ -70,7 +108,7 @@ final class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
getPhotoId(cursor),
getPhotoUri(cursor),
name,
- 0);
+ LetterTileDrawable.TYPE_DEFAULT);
String photoDescription =
context.getString(com.android.contacts.common.R.string.description_quick_contact_for, name);
@@ -79,44 +117,48 @@ final class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
// Always show the view holder's header if it's the first item in the list. Otherwise, compare
// it to the previous element and only show the anchored header if the row elements fall into
// the same sublists.
- if (position == 0) {
- contactViewHolder.bind(header, name, contactUri, true);
- } else {
- boolean showHeader = !header.equals(getHeaderString(position - 1));
- contactViewHolder.bind(header, name, contactUri, showHeader);
- }
+ boolean showHeader = position == 0 || !header.equals(getHeaderString(position - 1));
+ contactViewHolder.bind(header, name, contactUri, showHeader);
}
- public void refreshHeaders() {
- for (ContactViewHolder holder : holderMap.keySet()) {
- onBindViewHolder(holder, holderMap.get(holder));
+ /**
+ * Returns {@link #ADD_CONTACT_VIEW_TYPE} if the adapter was initialized with {@link
+ * Header#ADD_CONTACT} and the position is 0. Otherwise, {@link #CONTACT_VIEW_TYPE}.
+ */
+ @Override
+ public @ContactsViewType int getItemViewType(int position) {
+ if (header != Header.NONE && position == 0) {
+ return ADD_CONTACT_VIEW_TYPE;
}
+ return CONTACT_VIEW_TYPE;
}
@Override
- public int getItemCount() {
- return cursor == null ? 0 : cursor.getCount();
- }
-
- public String getHeader(int position) {
- return getHolderAt(position).getHeader();
- }
-
- public TextView getHeaderView(int position) {
- return getHolderAt(position).getHeaderView();
+ public void onViewRecycled(RecyclerView.ViewHolder contactViewHolder) {
+ super.onViewRecycled(contactViewHolder);
+ if (contactViewHolder instanceof ContactViewHolder) {
+ holderMap.remove(contactViewHolder);
+ }
}
- public void setHeaderVisibility(int position, int visibility) {
- getHolderAt(position).getHeaderView().setVisibility(visibility);
+ void refreshHeaders() {
+ for (ContactViewHolder holder : holderMap.keySet()) {
+ int position = holderMap.get(holder);
+ boolean showHeader =
+ position == 0 || !getHeaderString(position).equals(getHeaderString(position - 1));
+ int visibility = showHeader ? View.VISIBLE : View.INVISIBLE;
+ holder.getHeaderView().setVisibility(visibility);
+ }
}
- private ContactViewHolder getHolderAt(int position) {
- for (ContactViewHolder holder : holderMap.keySet()) {
- if (holderMap.get(holder) == position) {
- return holder;
- }
+ @Override
+ public int getItemCount() {
+ int count = cursor == null || cursor.isClosed() ? 0 : cursor.getCount();
+ // Manually insert the header if one exists.
+ if (header != Header.NONE) {
+ count++;
}
- throw Assert.createIllegalStateFailException("No holder for position: " + position);
+ return count;
}
private static String getDisplayName(Cursor cursor) {
@@ -138,7 +180,14 @@ final class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
return Contacts.getLookupUri(contactId, lookupKey);
}
- private String getHeaderString(int position) {
+ String getHeaderString(int position) {
+ if (header != Header.NONE) {
+ if (position == 0) {
+ return "+";
+ }
+ position--;
+ }
+
int index = -1;
int sum = 0;
while (sum <= position) {
diff --git a/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java b/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
index 6d4d21079..a22f7eb39 100644
--- a/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
+++ b/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
@@ -29,7 +29,7 @@ final class ContactsCursorLoader extends CursorLoader {
public static final int CONTACT_PHOTO_URI = 3;
public static final int CONTACT_LOOKUP_KEY = 4;
- public static final String[] CONTACTS_PROJECTION =
+ public static final String[] CONTACTS_PROJECTION_DISPLAY_NAME_PRIMARY =
new String[] {
Contacts._ID, // 0
Contacts.DISPLAY_NAME_PRIMARY, // 1
@@ -38,16 +38,35 @@ final class ContactsCursorLoader extends CursorLoader {
Contacts.LOOKUP_KEY, // 4
};
- public ContactsCursorLoader(Context context) {
+ public static final String[] CONTACTS_PROJECTION_DISPLAY_NAME_ALTERNATIVE =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 1
+ Contacts.PHOTO_ID, // 2
+ Contacts.PHOTO_THUMBNAIL_URI, // 3
+ Contacts.LOOKUP_KEY, // 4
+ };
+
+ private ContactsCursorLoader(Context context, String[] contactProjection, String sortKey) {
super(
context,
Contacts.CONTENT_URI
.buildUpon()
.appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true")
.build(),
- CONTACTS_PROJECTION,
- null,
+ contactProjection,
+ contactProjection[CONTACT_DISPLAY_NAME] + " IS NOT NULL",
null,
- Contacts.SORT_KEY_PRIMARY + " ASC");
+ sortKey + " ASC");
+ }
+
+ public static ContactsCursorLoader createInstanceDisplayNamePrimary(
+ Context context, String sortKey) {
+ return new ContactsCursorLoader(context, CONTACTS_PROJECTION_DISPLAY_NAME_PRIMARY, sortKey);
+ }
+
+ public static ContactsCursorLoader createInstanceDisplayNameAlternative(
+ Context context, String sortKey) {
+ return new ContactsCursorLoader(context, CONTACTS_PROJECTION_DISPLAY_NAME_ALTERNATIVE, sortKey);
}
}
diff --git a/java/com/android/dialer/contactsfragment/ContactsFragment.java b/java/com/android/dialer/contactsfragment/ContactsFragment.java
index ea662fc89..ddf00b358 100644
--- a/java/com/android/dialer/contactsfragment/ContactsFragment.java
+++ b/java/com/android/dialer/contactsfragment/ContactsFragment.java
@@ -19,60 +19,194 @@ package com.android.dialer.contactsfragment;
import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Loader;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
+import android.support.v13.app.FragmentCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Recycler;
+import android.support.v7.widget.RecyclerView.State;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnScrollChangeListener;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.preference.ContactsPreferences.ChangeListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.performancereport.PerformanceReport;
+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 LoaderCallbacks<Cursor>, OnScrollChangeListener {
+ implements LoaderCallbacks<Cursor>,
+ OnScrollChangeListener,
+ OnEmptyViewActionButtonClickedListener,
+ ChangeListener {
+ /** IntDef to define the OnClick action for contact rows. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ClickAction.INVALID, ClickAction.OPEN_CONTACT_CARD})
+ public @interface ClickAction {
+ int INVALID = 0;
+ /** Open contact card on click. */
+ int OPEN_CONTACT_CARD = 1;
+ }
+
+ /** 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;
+ }
+
+ public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
+
+ private static final String EXTRA_HEADER = "extra_header";
+ private static final String EXTRA_CLICK_ACTION = "extra_click_action";
+
+ private FastScroller fastScroller;
private TextView anchoredHeader;
private RecyclerView recyclerView;
private LinearLayoutManager manager;
private ContactsAdapter adapter;
+ private EmptyContentView emptyContentView;
+
+ private ContactsPreferences contactsPrefs;
+ private @Header int header;
+ private @ClickAction int clickAction;
+
+ /**
+ * 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>{@link ClickAction#OPEN_CONTACT_CARD} to 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>{@link ClickAction#SET_RESULT_AND_FINISH} to send a selected contact to the previous
+ * activity.
+ * </ul>
+ *
+ * @param header determines the type of header inserted at position 0 in the contacts list
+ * @param clickAction defines the on click actions on rows that represent contacts
+ */
+ public static ContactsFragment newInstance(@Header int header, @ClickAction int clickAction) {
+ Assert.checkArgument(clickAction != ClickAction.INVALID, "Invalid click action");
+ ContactsFragment fragment = new ContactsFragment();
+ Bundle args = new Bundle();
+ args.putInt(EXTRA_HEADER, header);
+ args.putInt(EXTRA_CLICK_ACTION, clickAction);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @SuppressWarnings("WrongConstant")
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ contactsPrefs = new ContactsPreferences(getContext());
+ contactsPrefs.registerChangeListener(this);
+ header = getArguments().getInt(EXTRA_HEADER);
+ clickAction = getArguments().getInt(EXTRA_CLICK_ACTION);
+ }
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contacts, container, false);
- anchoredHeader = (TextView) view.findViewById(R.id.header);
- manager = new LinearLayoutManager(getContext());
+ fastScroller = view.findViewById(R.id.fast_scroller);
+ anchoredHeader = view.findViewById(R.id.header);
+ recyclerView = view.findViewById(R.id.recycler_view);
- // TODO: Handle contacts permission denied view
- // TODO: Handle 0 contacts layout
- recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
- recyclerView.setLayoutManager(manager);
- getLoaderManager().initLoader(0, null, this);
+ emptyContentView = view.findViewById(R.id.empty_list_view);
+ emptyContentView.setImage(R.drawable.empty_contacts);
+ emptyContentView.setActionClickedListener(this);
if (PermissionsUtil.hasContactsReadPermissions(getContext())) {
getLoaderManager().initLoader(0, null, this);
+ } else {
+ emptyContentView.setDescription(R.string.permission_no_contacts);
+ emptyContentView.setActionLabel(R.string.permission_single_turn_on);
+ emptyContentView.setVisibility(View.VISIBLE);
}
return view;
}
@Override
+ public void onChange() {
+ if (getActivity() != null && isAdded()) {
+ getLoaderManager().restartLoader(0, null, this);
+ }
+ }
+
+ /** @return a loader according to sort order and display order. */
+ @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- return new ContactsCursorLoader(getContext());
+ boolean sortOrderPrimary =
+ (contactsPrefs.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY);
+ boolean displayOrderPrimary =
+ (contactsPrefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+
+ String sortKey = sortOrderPrimary ? Contacts.SORT_KEY_PRIMARY : Contacts.SORT_KEY_ALTERNATIVE;
+ return displayOrderPrimary
+ ? ContactsCursorLoader.createInstanceDisplayNamePrimary(getContext(), sortKey)
+ : ContactsCursorLoader.createInstanceDisplayNameAlternative(getContext(), sortKey);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- // TODO setup fast scroller.
- adapter = new ContactsAdapter(getContext(), cursor);
- recyclerView.setAdapter(adapter);
- if (adapter.getItemCount() > 1) {
- recyclerView.setOnScrollChangeListener(this);
+ if (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 = new ContactsAdapter(getContext(), cursor, header, clickAction);
+ manager =
+ new LinearLayoutManager(getContext()) {
+ @Override
+ public void onLayoutChildren(Recycler recycler, 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);
+ recyclerView.setAdapter(adapter);
+ PerformanceReport.logOnScrollStateChange(recyclerView);
+ fastScroller.setup(adapter, manager);
}
}
@@ -81,6 +215,7 @@ public class ContactsFragment extends Fragment
recyclerView.setAdapter(null);
recyclerView.setOnScrollChangeListener(null);
adapter = null;
+ contactsPrefs.unregisterChangeListener();
}
/*
@@ -95,8 +230,14 @@ public class ContactsFragment extends Fragment
*/
@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);
// 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.
@@ -104,15 +245,57 @@ public class ContactsFragment extends Fragment
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));
+ FragmentCompat.requestPermissions(
+ this, deniedPermissions, READ_CONTACTS_PERMISSION_REQUEST_CODE);
+ }
+
+ } 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 {
- boolean showAnchor =
- adapter.getHeader(firstVisibleItem).equals(adapter.getHeader(firstCompletelyVisible));
- anchoredHeader.setText(adapter.getHeader(firstCompletelyVisible));
- anchoredHeader.setVisibility(showAnchor ? View.VISIBLE : View.INVISIBLE);
-
- int rowHeaderVisibility = showAnchor ? View.INVISIBLE : View.VISIBLE;
- adapter.setHeaderVisibility(firstVisibleItem, rowHeaderVisibility);
- adapter.setHeaderVisibility(firstCompletelyVisible, rowHeaderVisibility);
+ throw Assert.createIllegalStateFailException("Invalid empty content view action label.");
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ // Force a refresh of the data since we were missing the permission before this.
+ emptyContentView.setVisibility(View.GONE);
+ getLoaderManager().initLoader(0, null, this);
+ }
}
}
}
diff --git a/java/com/android/dialer/contactsfragment/FastScroller.java b/java/com/android/dialer/contactsfragment/FastScroller.java
new file mode 100644
index 000000000..2a86a3bb6
--- /dev/null
+++ b/java/com/android/dialer/contactsfragment/FastScroller.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017 The Android Open Source 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 android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+/** Widget to add fast scrolling to {@link ContactsFragment}. */
+public class FastScroller extends RelativeLayout {
+
+ private final int touchTargetWidth;
+
+ private ContactsAdapter adapter;
+ private LinearLayoutManager layoutManager;
+
+ private TextView container;
+ private View scrollBar;
+
+ private boolean dragStarted;
+
+ public FastScroller(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ touchTargetWidth =
+ context.getResources().getDimensionPixelSize(R.dimen.fast_scroller_touch_target_width);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ container = findViewById(R.id.fast_scroller_container);
+ scrollBar = findViewById(R.id.fast_scroller_scroll_bar);
+ }
+
+ void setup(ContactsAdapter adapter, LinearLayoutManager layoutManager) {
+ this.adapter = adapter;
+ this.layoutManager = layoutManager;
+ setVisibility(VISIBLE);
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ // Don't override if touch event isn't within desired touch target and dragging hasn't started.
+ if (!dragStarted && getWidth() - touchTargetWidth - event.getX() > 0) {
+ return super.onTouchEvent(event);
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ dragStarted = true;
+ container.setVisibility(VISIBLE);
+ scrollBar.setSelected(true);
+ // fall through
+ case MotionEvent.ACTION_MOVE:
+ setContainerAndScrollBarPosition(event.getY());
+ setRecyclerViewPosition(event.getY());
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ dragStarted = false;
+ container.setVisibility(INVISIBLE);
+ scrollBar.setSelected(false);
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private void setRecyclerViewPosition(float y) {
+ final int itemCount = adapter.getItemCount();
+ float scrolledPosition = getScrolledPercentage(y) * (float) itemCount;
+ int targetPos = getValueInRange(0, itemCount - 1, (int) scrolledPosition);
+ layoutManager.scrollToPositionWithOffset(targetPos, 0);
+ container.setText(adapter.getHeaderString(targetPos));
+ adapter.refreshHeaders();
+ }
+
+ // Returns a float in range [0, 1] which represents the position of the scroller.
+ private float getScrolledPercentage(float y) {
+ if (scrollBar.getY() == 0) {
+ return 0f;
+ } else if (scrollBar.getY() + scrollBar.getHeight() >= getHeight()) {
+ return 1f;
+ } else {
+ return y / (float) getHeight();
+ }
+ }
+
+ private int getValueInRange(int min, int max, int value) {
+ int minimum = Math.max(min, value);
+ return Math.min(minimum, max);
+ }
+
+ void updateContainerAndScrollBarPosition(RecyclerView recyclerView) {
+ if (!scrollBar.isSelected()) {
+ int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
+ int verticalScrollRange = recyclerView.computeVerticalScrollRange();
+ float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - getHeight());
+ setContainerAndScrollBarPosition(getHeight() * proportion);
+ }
+ }
+
+ private void setContainerAndScrollBarPosition(float y) {
+ int scrollBarHeight = scrollBar.getHeight();
+ int containerHeight = container.getHeight();
+ scrollBar.setY(
+ getValueInRange(0, getHeight() - scrollBarHeight, (int) (y - scrollBarHeight / 2)));
+ container.setY(
+ getValueInRange(
+ 0, getHeight() - containerHeight - scrollBarHeight / 2, (int) (y - containerHeight)));
+ }
+}
diff --git a/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_container_background.xml b/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_container_background.xml
new file mode 100644
index 000000000..a7b227799
--- /dev/null
+++ b/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_container_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source 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
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/dialer_theme_color"/>
+ <size
+ android:height="@dimen/fast_scroller_container_size"
+ android:width="@dimen/fast_scroller_container_size"/>
+ <corners
+ android:topLeftRadius="@dimen/fast_scroller_container_corner_radius"
+ android:topRightRadius="@dimen/fast_scroller_container_corner_radius"
+ android:bottomLeftRadius="@dimen/fast_scroller_bottom_left_corner_radius"
+ android:bottomRightRadius="@dimen/fast_scroller_bottom_right_corner_radius"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_scroll_bar.xml b/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_scroll_bar.xml
new file mode 100644
index 000000000..a3e0c25c7
--- /dev/null
+++ b/java/com/android/dialer/contactsfragment/res/drawable/fast_scroller_scroll_bar.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source 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
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/dialer_theme_color"/>
+ <size android:height="32dp" android:width="4dp"/>
+ <corners android:radius="2dp"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/dialer_secondary_text_color"/>
+ <size android:height="32dp" android:width="4dp"/>
+ <corners android:radius="2dp"/>
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/java/com/android/dialer/contactsfragment/res/layout/add_contact_row.xml b/java/com/android/dialer/contactsfragment/res/layout/add_contact_row.xml
new file mode 100644
index 000000000..dbc7cafb8
--- /dev/null
+++ b/java/com/android/dialer/contactsfragment/res/layout/add_contact_row.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source 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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/click_target"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/row_height"
+ android:layout_marginTop="8dp"
+ android:layout_marginStart="@dimen/header_width"
+ android:layout_marginEnd="@dimen/row_end_margin"
+ android:paddingTop="@dimen/row_top_bottom_padding"
+ android:paddingBottom="@dimen/row_top_bottom_padding"
+ android:paddingStart="@dimen/row_start_padding"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/photo"
+ android:layout_width="@dimen/photo_size"
+ android:layout_height="@dimen/photo_size"
+ android:src="@drawable/quantum_ic_person_add_white_24"
+ android:tint="@color/dialer_theme_color"
+ android:scaleType="center"/>
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/text_padding_start"
+ android:paddingEnd="@dimen/text_padding_end"
+ android:gravity="center_vertical"
+ android:fontFamily="sans-serif"
+ android:text="@string/all_contacts_empty_add_contact_action"
+ style="@style/PrimaryText"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml b/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
index af87c7f18..9e829fee4 100644
--- a/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
+++ b/java/com/android/dialer/contactsfragment/res/layout/contact_row.xml
@@ -43,11 +43,13 @@
<TextView
android:id="@+id/contact_name"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="@dimen/text_padding_start"
android:paddingEnd="@dimen/text_padding_end"
- android:gravity="center_vertical|start"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:gravity="center_vertical"
android:textSize="@dimen/text_size"
android:textColor="@color/dialer_primary_text_color"
android:fontFamily="sans-serif"/>
diff --git a/java/com/android/dialer/contactsfragment/res/layout/fragment_contacts.xml b/java/com/android/dialer/contactsfragment/res/layout/fragment_contacts.xml
index 67b490f03..3d58aad0d 100644
--- a/java/com/android/dialer/contactsfragment/res/layout/fragment_contacts.xml
+++ b/java/com/android/dialer/contactsfragment/res/layout/fragment_contacts.xml
@@ -23,8 +23,47 @@
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/background_dialer_white"/>
+ android:background="@color/background_dialer_light"/>
+
+ <!-- Scrollbars are always on the right side of the screen. Layouts should use Rights/Left instead
+ of Start/End -->
+ <com.android.dialer.contactsfragment.FastScroller
+ android:id="@+id/fast_scroller"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:clipChildren="false"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/fast_scroller_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toLeftOf="@+id/fast_scroller_scroll_bar"
+ android:gravity="center"
+ android:textSize="48sp"
+ android:textColor="@color/background_dialer_white"
+ android:visibility="gone"
+ android:background="@drawable/fast_scroller_container_background"/>
+
+ <ImageView
+ android:id="@+id/fast_scroller_scroll_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:layout_alignParentRight="true"
+ android:paddingRight="16dp"
+ android:src="@drawable/fast_scroller_scroll_bar" />
+ </com.android.dialer.contactsfragment.FastScroller>
<!-- Anchored header view -->
<include layout="@layout/header"/>
+
+ <com.android.dialer.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
</FrameLayout>
diff --git a/java/com/android/dialer/contactsfragment/res/values/dimens.xml b/java/com/android/dialer/contactsfragment/res/values/dimens.xml
index 00d7c6d7e..f120014e2 100644
--- a/java/com/android/dialer/contactsfragment/res/values/dimens.xml
+++ b/java/com/android/dialer/contactsfragment/res/values/dimens.xml
@@ -25,4 +25,11 @@
<dimen name="text_padding_start">16dp</dimen>
<dimen name="text_padding_end">8dp</dimen>
<dimen name="text_size">16sp</dimen>
-</resources>
+
+ <dimen name="fast_scroller_touch_target_width">20dp</dimen>
+
+ <dimen name="fast_scroller_container_size">88dp</dimen>
+ <dimen name="fast_scroller_container_corner_radius">44dp</dimen>
+ <dimen name="fast_scroller_bottom_right_corner_radius">0px</dimen>
+ <dimen name="fast_scroller_bottom_left_corner_radius">44dp</dimen>
+</resources> \ No newline at end of file