Loading java/com/android/dialer/app/DialtactsActivity.java +15 −1 Original line number Diff line number Diff line Loading @@ -491,6 +491,11 @@ public class DialtactsActivity extends TransactionSafeActivity mP13nLogger = P13nLogging.get(getApplicationContext()); mP13nRanker = P13nRanking.get(getApplicationContext()); Trace.endSection(); // Update the new search fragment to the correct position and the ActionBar's visibility. if (ConfigProviderBindings.get(this).getBoolean("enable_new_search_fragment", false)) { updateSearchFragmentPosition(); } } @NonNull Loading Loading @@ -1550,6 +1555,10 @@ public class DialtactsActivity extends TransactionSafeActivity @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // FAB does not move with the new favorites UI if (newFavoritesIsEnabled()) { return; } int tabIndex = mListsFragment.getCurrentTabIndex(); // Scroll the button from center to end when moving from the Speed Dial to Call History tab. Loading Loading @@ -1610,7 +1619,8 @@ public class DialtactsActivity extends TransactionSafeActivity @VisibleForTesting public int getFabAlignment() { if (!mIsLandscape if (!newFavoritesIsEnabled() && !mIsLandscape && !isInSearchUi() && mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { return FloatingActionButtonController.ALIGN_MIDDLE; Loading Loading @@ -1741,4 +1751,8 @@ public class DialtactsActivity extends TransactionSafeActivity static void setVoiceSearchEnabledForTest(Optional<Boolean> enabled) { sVoiceSearchEnabledForTest = enabled; } private boolean newFavoritesIsEnabled() { return ConfigProviderBindings.get(this).getBoolean("enable_new_favorites_tab", false); } } java/com/android/dialer/speeddial/FavoritesViewHolder.java 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.FrameLayout; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.common.Assert; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder implements OnClickListener, OnLongClickListener { private final FavoriteContactsListener listener; private final QuickContactBadge photoView; private final TextView nameView; private final TextView phoneType; private final FrameLayout videoCallIcon; private boolean isVideoCall; private String number; public FavoritesViewHolder(View view, FavoriteContactsListener listener) { super(view); photoView = view.findViewById(R.id.avatar); nameView = view.findViewById(R.id.name); phoneType = view.findViewById(R.id.phone_type); videoCallIcon = view.findViewById(R.id.video_call_container); view.setOnClickListener(this); view.setOnLongClickListener(this); photoView.setClickable(false); this.listener = listener; } public void bind(Context context, Cursor cursor) { Assert.checkArgument(cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1); isVideoCall = false; // TODO(calderwoodra): get from disambig data number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); String lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY); Uri contactUri = Contacts.getLookupUri(contactId, lookupKey); String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI); ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( photoView, contactUri, cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID), photoUri == null ? null : Uri.parse(photoUri), name, LetterTileDrawable.TYPE_DEFAULT); nameView.setText(name); phoneType.setText(getLabel(context.getResources(), cursor)); videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); } // TODO(calderwoodra): handle CNAP and cequint types. // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType private static String getLabel(Resources resources, Cursor cursor) { int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE); String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL); // Returns empty label instead of "custom" if the custom label is empty. if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { return ""; } return (String) Phone.getTypeLabel(resources, numberType, numberLabel); } @Override public void onClick(View v) { listener.onClick(number, isVideoCall); } @Override public boolean onLongClick(View v) { // TODO(calderwoodra): implement drag and drop logic listener.onLongClick(number); return true; } /** Listener/callback for {@link FavoritesViewHolder} actions. */ public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); /** Called when the user long clicks on a favorite contact. */ void onLongClick(String number); } } java/com/android/dialer/speeddial/HeaderViewHolder.java 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.support.annotation.StringRes; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; /** ViewHolder for headers in {@link SpeedDialFragment}. */ public class HeaderViewHolder extends RecyclerView.ViewHolder implements OnClickListener { private final SpeedDialHeaderListener listener; private final TextView headerText; private final Button addButton; public HeaderViewHolder(View view, SpeedDialHeaderListener listener) { super(view); this.listener = listener; headerText = view.findViewById(R.id.speed_dial_header_text); addButton = view.findViewById(R.id.speed_dial_add_button); addButton.setOnClickListener(this); } public void setHeaderText(@StringRes int header) { headerText.setText(header); } public void showAddButton(boolean show) { addButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void onClick(View v) { listener.onAddFavoriteClicked(); } /** Listener/Callback for {@link HeaderViewHolder} parents. */ public interface SpeedDialHeaderListener { /** Called when the user wants to add a contact to their favorites. */ void onAddFavoriteClicked(); } } java/com/android/dialer/speeddial/SpeedDialAdapter.java 0 → 100644 +141 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.content.Context; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.LayoutManager; import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.common.Assert; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SpeedDialCursor.RowType; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; /** * RecyclerView adapter for {@link SpeedDialFragment}. * * <p>Displays a list in the following order: * * <ol> * <li>Favorite contacts header (with add button) * <li>Favorite contacts * <li>Suggested contacts header * <li>Suggested contacts * </ol> */ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private final Context context; private final FavoriteContactsListener favoritesListener; private final SuggestedContactsListener suggestedListener; private final SpeedDialHeaderListener headerListener; private SpeedDialCursor cursor; public SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, SuggestedContactsListener suggestedListener, SpeedDialHeaderListener headerListener) { this.context = context; this.favoritesListener = favoritesListener; this.suggestedListener = suggestedListener; this.headerListener = headerListener; } @Override public int getItemViewType(int position) { return cursor.getRowType(position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(context); if (viewType == RowType.STARRED) { return new FavoritesViewHolder( inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener); } else if (viewType == RowType.SUGGESTION) { return new SuggestionViewHolder( inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); } else if (viewType == RowType.HEADER) { return new HeaderViewHolder( inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener); } else { throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { cursor.moveToPosition(position); switch (cursor.getRowType(position)) { case RowType.HEADER: ((HeaderViewHolder) holder).setHeaderText(cursor.getHeader()); ((HeaderViewHolder) holder).showAddButton(cursor.hasFavorites() && position == 0); break; case RowType.STARRED: ((FavoritesViewHolder) holder).bind(context, cursor); break; case RowType.SUGGESTION: ((SuggestionViewHolder) holder).bind(context, cursor); break; default: throw Assert.createIllegalStateFailException("Invalid view holder: " + holder); } } @Override public int getItemCount() { return cursor == null || cursor.isClosed() ? 0 : cursor.getCount(); } public void setCursor(SpeedDialCursor cursor) { this.cursor = cursor; notifyDataSetChanged(); } LayoutManager getLayoutManager(Context context) { GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); layoutManager.setSpanSizeLookup( new SpanSizeLookup() { @Override public int getSpanSize(int position) { return SpeedDialAdapter.this.getSpanSize(position); } }); return layoutManager; } @VisibleForTesting int getSpanSize(int position) { switch (cursor.getRowType(position)) { case RowType.SUGGESTION: case RowType.HEADER: return 3; // span the whole screen case RowType.STARRED: return 1; // span 1/3 of the screen default: throw Assert.createIllegalStateFailException( "Invalid row type: " + cursor.getRowType(position)); } } } java/com/android/dialer/speeddial/SpeedDialCursor.java 0 → 100644 +152 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.annotation.SuppressLint; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.support.annotation.IntDef; import android.support.annotation.StringRes; import com.android.dialer.common.Assert; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** Cursor for favorites contacts. */ final class SpeedDialCursor extends MergeCursor { /** * Caps the speed dial list to contain at most 20 contacts, including favorites and suggestions. * It is only a soft limit though, for the case that there are more than 20 favorite contacts. */ private static final int SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT = 20; private static final String[] HEADER_CURSOR_PROJECTION = {"header"}; private static final int HEADER_COLUMN_POSITION = 0; private boolean hasFavorites; @Retention(RetentionPolicy.SOURCE) @IntDef({RowType.HEADER, RowType.STARRED, RowType.SUGGESTION}) @interface RowType { int HEADER = 0; int STARRED = 1; int SUGGESTION = 2; } public static SpeedDialCursor newInstance(Cursor strequentCursor) { if (strequentCursor == null || strequentCursor.getCount() == 0) { return null; } SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor)); strequentCursor.close(); return cursor; } private static Cursor[] buildCursors(Cursor strequentCursor) { MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); strequentCursor.moveToPosition(-1); while (strequentCursor.moveToNext()) { if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0) { continue; } if (strequentCursor.getPosition() != 0) { long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); int position = strequentCursor.getPosition(); boolean duplicate = false; // Iterate backwards through the cursor to check that this isn't a duplicate contact // TODO(calderwoodra): improve this algorithm (currently O(n^2)). while (strequentCursor.moveToPrevious() && !duplicate) { duplicate |= strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId; } strequentCursor.moveToPosition(position); if (duplicate) { continue; } } if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) { // Since all starred contacts come before each non-starred contact, it's safe to assume that // this list will never exceed the soft limit unless there are more starred contacts than // the limit permits. StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); } } List<Cursor> cursorList = new ArrayList<>(); if (starred.getCount() > 0) { cursorList.add(createHeaderCursor(R.string.favorites_header)); cursorList.add(starred); } if (suggestions.getCount() > 0) { cursorList.add(createHeaderCursor(R.string.suggestions_header)); cursorList.add(suggestions); } return cursorList.toArray(new Cursor[cursorList.size()]); } private static Cursor createHeaderCursor(@StringRes int header) { MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); return cursor; } @RowType int getRowType(int position) { moveToPosition(position); if (getColumnCount() == 1) { return RowType.HEADER; } else if (getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { return RowType.STARRED; } else { return RowType.SUGGESTION; } } @SuppressLint("DefaultLocale") @StringRes int getHeader() { if (getRowType(getPosition()) != RowType.HEADER) { throw Assert.createIllegalStateFailException( String.format("Current position (%d) is not a header.", getPosition())); } return getInt(HEADER_COLUMN_POSITION); } public boolean hasFavorites() { return hasFavorites; } private SpeedDialCursor(Cursor[] cursors) { super(cursors); for (Cursor cursor : cursors) { cursor.moveToFirst(); if (cursor.getColumnCount() != 1 && cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { hasFavorites = true; break; } } } } Loading
java/com/android/dialer/app/DialtactsActivity.java +15 −1 Original line number Diff line number Diff line Loading @@ -491,6 +491,11 @@ public class DialtactsActivity extends TransactionSafeActivity mP13nLogger = P13nLogging.get(getApplicationContext()); mP13nRanker = P13nRanking.get(getApplicationContext()); Trace.endSection(); // Update the new search fragment to the correct position and the ActionBar's visibility. if (ConfigProviderBindings.get(this).getBoolean("enable_new_search_fragment", false)) { updateSearchFragmentPosition(); } } @NonNull Loading Loading @@ -1550,6 +1555,10 @@ public class DialtactsActivity extends TransactionSafeActivity @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // FAB does not move with the new favorites UI if (newFavoritesIsEnabled()) { return; } int tabIndex = mListsFragment.getCurrentTabIndex(); // Scroll the button from center to end when moving from the Speed Dial to Call History tab. Loading Loading @@ -1610,7 +1619,8 @@ public class DialtactsActivity extends TransactionSafeActivity @VisibleForTesting public int getFabAlignment() { if (!mIsLandscape if (!newFavoritesIsEnabled() && !mIsLandscape && !isInSearchUi() && mListsFragment.getCurrentTabIndex() == DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL) { return FloatingActionButtonController.ALIGN_MIDDLE; Loading Loading @@ -1741,4 +1751,8 @@ public class DialtactsActivity extends TransactionSafeActivity static void setVoiceSearchEnabledForTest(Optional<Boolean> enabled) { sVoiceSearchEnabledForTest = enabled; } private boolean newFavoritesIsEnabled() { return ConfigProviderBindings.get(this).getBoolean("enable_new_favorites_tab", false); } }
java/com/android/dialer/speeddial/FavoritesViewHolder.java 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.FrameLayout; import android.widget.QuickContactBadge; import android.widget.TextView; import com.android.dialer.common.Assert; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder implements OnClickListener, OnLongClickListener { private final FavoriteContactsListener listener; private final QuickContactBadge photoView; private final TextView nameView; private final TextView phoneType; private final FrameLayout videoCallIcon; private boolean isVideoCall; private String number; public FavoritesViewHolder(View view, FavoriteContactsListener listener) { super(view); photoView = view.findViewById(R.id.avatar); nameView = view.findViewById(R.id.name); phoneType = view.findViewById(R.id.phone_type); videoCallIcon = view.findViewById(R.id.video_call_container); view.setOnClickListener(this); view.setOnLongClickListener(this); photoView.setClickable(false); this.listener = listener; } public void bind(Context context, Cursor cursor) { Assert.checkArgument(cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1); isVideoCall = false; // TODO(calderwoodra): get from disambig data number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); String lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY); Uri contactUri = Contacts.getLookupUri(contactId, lookupKey); String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI); ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( photoView, contactUri, cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID), photoUri == null ? null : Uri.parse(photoUri), name, LetterTileDrawable.TYPE_DEFAULT); nameView.setText(name); phoneType.setText(getLabel(context.getResources(), cursor)); videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); } // TODO(calderwoodra): handle CNAP and cequint types. // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType private static String getLabel(Resources resources, Cursor cursor) { int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE); String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL); // Returns empty label instead of "custom" if the custom label is empty. if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) { return ""; } return (String) Phone.getTypeLabel(resources, numberType, numberLabel); } @Override public void onClick(View v) { listener.onClick(number, isVideoCall); } @Override public boolean onLongClick(View v) { // TODO(calderwoodra): implement drag and drop logic listener.onLongClick(number); return true; } /** Listener/callback for {@link FavoritesViewHolder} actions. */ public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); /** Called when the user long clicks on a favorite contact. */ void onLongClick(String number); } }
java/com/android/dialer/speeddial/HeaderViewHolder.java 0 → 100644 +60 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.support.annotation.StringRes; import android.support.v7.widget.RecyclerView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; /** ViewHolder for headers in {@link SpeedDialFragment}. */ public class HeaderViewHolder extends RecyclerView.ViewHolder implements OnClickListener { private final SpeedDialHeaderListener listener; private final TextView headerText; private final Button addButton; public HeaderViewHolder(View view, SpeedDialHeaderListener listener) { super(view); this.listener = listener; headerText = view.findViewById(R.id.speed_dial_header_text); addButton = view.findViewById(R.id.speed_dial_add_button); addButton.setOnClickListener(this); } public void setHeaderText(@StringRes int header) { headerText.setText(header); } public void showAddButton(boolean show) { addButton.setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void onClick(View v) { listener.onAddFavoriteClicked(); } /** Listener/Callback for {@link HeaderViewHolder} parents. */ public interface SpeedDialHeaderListener { /** Called when the user wants to add a contact to their favorites. */ void onAddFavoriteClicked(); } }
java/com/android/dialer/speeddial/SpeedDialAdapter.java 0 → 100644 +141 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.content.Context; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.LayoutManager; import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.common.Assert; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SpeedDialCursor.RowType; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; /** * RecyclerView adapter for {@link SpeedDialFragment}. * * <p>Displays a list in the following order: * * <ol> * <li>Favorite contacts header (with add button) * <li>Favorite contacts * <li>Suggested contacts header * <li>Suggested contacts * </ol> */ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private final Context context; private final FavoriteContactsListener favoritesListener; private final SuggestedContactsListener suggestedListener; private final SpeedDialHeaderListener headerListener; private SpeedDialCursor cursor; public SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, SuggestedContactsListener suggestedListener, SpeedDialHeaderListener headerListener) { this.context = context; this.favoritesListener = favoritesListener; this.suggestedListener = suggestedListener; this.headerListener = headerListener; } @Override public int getItemViewType(int position) { return cursor.getRowType(position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(context); if (viewType == RowType.STARRED) { return new FavoritesViewHolder( inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener); } else if (viewType == RowType.SUGGESTION) { return new SuggestionViewHolder( inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); } else if (viewType == RowType.HEADER) { return new HeaderViewHolder( inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener); } else { throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { cursor.moveToPosition(position); switch (cursor.getRowType(position)) { case RowType.HEADER: ((HeaderViewHolder) holder).setHeaderText(cursor.getHeader()); ((HeaderViewHolder) holder).showAddButton(cursor.hasFavorites() && position == 0); break; case RowType.STARRED: ((FavoritesViewHolder) holder).bind(context, cursor); break; case RowType.SUGGESTION: ((SuggestionViewHolder) holder).bind(context, cursor); break; default: throw Assert.createIllegalStateFailException("Invalid view holder: " + holder); } } @Override public int getItemCount() { return cursor == null || cursor.isClosed() ? 0 : cursor.getCount(); } public void setCursor(SpeedDialCursor cursor) { this.cursor = cursor; notifyDataSetChanged(); } LayoutManager getLayoutManager(Context context) { GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); layoutManager.setSpanSizeLookup( new SpanSizeLookup() { @Override public int getSpanSize(int position) { return SpeedDialAdapter.this.getSpanSize(position); } }); return layoutManager; } @VisibleForTesting int getSpanSize(int position) { switch (cursor.getRowType(position)) { case RowType.SUGGESTION: case RowType.HEADER: return 3; // span the whole screen case RowType.STARRED: return 1; // span 1/3 of the screen default: throw Assert.createIllegalStateFailException( "Invalid row type: " + cursor.getRowType(position)); } } }
java/com/android/dialer/speeddial/SpeedDialCursor.java 0 → 100644 +152 −0 Original line number Diff line number Diff line /* * 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.speeddial; import android.annotation.SuppressLint; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.support.annotation.IntDef; import android.support.annotation.StringRes; import com.android.dialer.common.Assert; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** Cursor for favorites contacts. */ final class SpeedDialCursor extends MergeCursor { /** * Caps the speed dial list to contain at most 20 contacts, including favorites and suggestions. * It is only a soft limit though, for the case that there are more than 20 favorite contacts. */ private static final int SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT = 20; private static final String[] HEADER_CURSOR_PROJECTION = {"header"}; private static final int HEADER_COLUMN_POSITION = 0; private boolean hasFavorites; @Retention(RetentionPolicy.SOURCE) @IntDef({RowType.HEADER, RowType.STARRED, RowType.SUGGESTION}) @interface RowType { int HEADER = 0; int STARRED = 1; int SUGGESTION = 2; } public static SpeedDialCursor newInstance(Cursor strequentCursor) { if (strequentCursor == null || strequentCursor.getCount() == 0) { return null; } SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor)); strequentCursor.close(); return cursor; } private static Cursor[] buildCursors(Cursor strequentCursor) { MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); strequentCursor.moveToPosition(-1); while (strequentCursor.moveToNext()) { if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0) { continue; } if (strequentCursor.getPosition() != 0) { long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); int position = strequentCursor.getPosition(); boolean duplicate = false; // Iterate backwards through the cursor to check that this isn't a duplicate contact // TODO(calderwoodra): improve this algorithm (currently O(n^2)). while (strequentCursor.moveToPrevious() && !duplicate) { duplicate |= strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId; } strequentCursor.moveToPosition(position); if (duplicate) { continue; } } if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) { // Since all starred contacts come before each non-starred contact, it's safe to assume that // this list will never exceed the soft limit unless there are more starred contacts than // the limit permits. StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); } } List<Cursor> cursorList = new ArrayList<>(); if (starred.getCount() > 0) { cursorList.add(createHeaderCursor(R.string.favorites_header)); cursorList.add(starred); } if (suggestions.getCount() > 0) { cursorList.add(createHeaderCursor(R.string.suggestions_header)); cursorList.add(suggestions); } return cursorList.toArray(new Cursor[cursorList.size()]); } private static Cursor createHeaderCursor(@StringRes int header) { MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); return cursor; } @RowType int getRowType(int position) { moveToPosition(position); if (getColumnCount() == 1) { return RowType.HEADER; } else if (getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { return RowType.STARRED; } else { return RowType.SUGGESTION; } } @SuppressLint("DefaultLocale") @StringRes int getHeader() { if (getRowType(getPosition()) != RowType.HEADER) { throw Assert.createIllegalStateFailException( String.format("Current position (%d) is not a header.", getPosition())); } return getInt(HEADER_COLUMN_POSITION); } public boolean hasFavorites() { return hasFavorites; } private SpeedDialCursor(Cursor[] cursors) { super(cursors); for (Cursor cursor : cursors) { cursor.moveToFirst(); if (cursor.getColumnCount() != 1 && cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { hasFavorites = true; break; } } } }