diff options
Diffstat (limited to 'java/com/android/dialer/contactsfragment')
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 |