Loading java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; import com.android.incallui.calllocation.CallLocationComponent; Loading Loading @@ -67,6 +68,7 @@ public interface BaseDialerRootComponent PhoneLookupDatabaseComponent.HasComponent, PhoneNumberGeoUtilComponent.HasComponent, PreCallComponent.HasComponent, UiItemLoaderComponent.HasComponent, SimSuggestionComponent.HasComponent, SimulatorComponent.HasComponent, SpamComponent.HasComponent, Loading java/com/android/dialer/speeddial/FavoritesViewHolder.java +37 −45 Original line number Diff line number Diff line Loading @@ -17,13 +17,8 @@ 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; Loading @@ -31,8 +26,12 @@ 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; import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; import com.android.dialer.glidephotomanager.PhotoInfo; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.util.ArrayList; import java.util.List; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder Loading @@ -48,7 +47,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private boolean hasDefaultNumber; private boolean isVideoCall; private String number; private String lookupKey; private List<Channel> channels; public FavoritesViewHolder(View view, FavoriteContactsListener listener) { super(view); Loading @@ -62,44 +61,37 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder 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); public void bind(Context context, SpeedDialUiItem speedDialUiItem) { Assert.checkArgument(speedDialUiItem.isStarred()); String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); 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)); nameView.setText(speedDialUiItem.name()); hasDefaultNumber = speedDialUiItem.defaultChannel() != null; if (hasDefaultNumber) { channels = new ArrayList<>(); isVideoCall = speedDialUiItem.defaultChannel().isVideoTechnology(); number = speedDialUiItem.defaultChannel().number(); phoneType.setText(speedDialUiItem.defaultChannel().label()); videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); // TODO(calderwoodra): Update this to include communication avenues also hasDefaultNumber = cursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0; } else { channels = speedDialUiItem.channels(); isVideoCall = false; number = null; phoneType.setText(""); videoCallIcon.setVisibility(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); GlidePhotoManagerComponent.get(context) .glidePhotoManager() .loadQuickContactBadge( photoView, PhotoInfo.newBuilder() .setPhotoId(speedDialUiItem.photoId()) .setPhotoUri(speedDialUiItem.photoUri()) .setName(speedDialUiItem.name()) .setLookupUri( Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey()) .toString()) .build()); } @Override Loading @@ -107,7 +99,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder if (hasDefaultNumber) { listener.onClick(number, isVideoCall); } else { listener.onAmbiguousContactClicked(lookupKey); listener.onAmbiguousContactClicked(channels); } } Loading @@ -122,7 +114,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact that doesn't have a default number. */ void onAmbiguousContactClicked(String contactId); void onAmbiguousContactClicked(List<Channel> channels); /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); Loading java/com/android/dialer/speeddial/SpeedDialAdapter.java +84 −34 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * Copyright (C) 2018 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. Loading @@ -16,19 +16,30 @@ package com.android.dialer.speeddial; import android.annotation.TargetApi; import android.content.Context; import android.os.Build.VERSION_CODES; import android.support.annotation.IntDef; import android.support.annotation.NonNull; 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.support.v7.widget.RecyclerView.ViewHolder; import android.util.ArrayMap; 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; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * RecyclerView adapter for {@link SpeedDialFragment}. Loading @@ -42,14 +53,26 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen * <li>Suggested contacts * </ol> */ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @SuppressWarnings("AndroidApiChecker") @TargetApi(VERSION_CODES.N) public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @Retention(RetentionPolicy.SOURCE) @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION}) @interface RowType { int STARRED_HEADER = 0; int SUGGESTION_HEADER = 1; int STARRED = 2; int SUGGESTION = 3; } private final Context context; private final FavoriteContactsListener favoritesListener; private final SuggestedContactsListener suggestedListener; private final SpeedDialHeaderListener headerListener; private SpeedDialCursor cursor; private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>(); private List<SpeedDialUiItem> speedDialUiItems; public SpeedDialAdapter( Context context, Loading @@ -64,39 +87,45 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @Override public int getItemViewType(int position) { return cursor.getRowType(position); return positionToRowTypeMap.get(position); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(context); if (viewType == RowType.STARRED) { switch (viewType) { case RowType.STARRED: return new FavoritesViewHolder( inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener); } else if (viewType == RowType.SUGGESTION) { case RowType.SUGGESTION: return new SuggestionViewHolder( inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); } else if (viewType == RowType.HEADER) { case RowType.STARRED_HEADER: case RowType.SUGGESTION_HEADER: return new HeaderViewHolder( inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener); } else { default: 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; public void onBindViewHolder(@NonNull ViewHolder holder, int position) { switch (getItemViewType(position)) { case RowType.STARRED_HEADER: ((HeaderViewHolder) holder).setHeaderText(R.string.favorites_header); ((HeaderViewHolder) holder).showAddButton(true); return; case RowType.SUGGESTION_HEADER: ((HeaderViewHolder) holder).setHeaderText(R.string.suggestions_header); ((HeaderViewHolder) holder).showAddButton(false); return; case RowType.STARRED: ((FavoritesViewHolder) holder).bind(context, cursor); ((FavoritesViewHolder) holder).bind(context, speedDialUiItems.get(position - 1)); break; case RowType.SUGGESTION: ((SuggestionViewHolder) holder).bind(context, cursor); ((SuggestionViewHolder) holder).bind(context, speedDialUiItems.get(position - 2)); break; default: throw Assert.createIllegalStateFailException("Invalid view holder: " + holder); Loading @@ -105,15 +134,35 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @Override public int getItemCount() { return cursor == null || cursor.isClosed() ? 0 : cursor.getCount(); return positionToRowTypeMap.size(); } public void setCursor(SpeedDialCursor cursor) { this.cursor = cursor; notifyDataSetChanged(); public void setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems) { speedDialUiItems = new ArrayList<>(); speedDialUiItems.addAll(immutableSpeedDialUiItems); speedDialUiItems.sort((o1, o2) -> Boolean.compare(o2.isStarred(), o1.isStarred())); positionToRowTypeMap.clear(); if (speedDialUiItems.isEmpty()) { return; } // Show the add favorites even if there are no favorite contacts positionToRowTypeMap.put(0, RowType.STARRED_HEADER); int positionOfSuggestionHeader = 1; for (int i = 0; i < speedDialUiItems.size(); i++) { if (speedDialUiItems.get(i).isStarred()) { positionToRowTypeMap.put(i + 1, RowType.STARRED); // +1 for the header positionOfSuggestionHeader++; } else { positionToRowTypeMap.put(i + 2, RowType.SUGGESTION); // +2 for both headers } } if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) { positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER); } } LayoutManager getLayoutManager(Context context) { /* package-private */ LayoutManager getLayoutManager(Context context) { GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); layoutManager.setSpanSizeLookup( new SpanSizeLookup() { Loading @@ -127,15 +176,16 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @VisibleForTesting int getSpanSize(int position) { switch (cursor.getRowType(position)) { switch (getItemViewType(position)) { case RowType.SUGGESTION: case RowType.HEADER: case RowType.STARRED_HEADER: case RowType.SUGGESTION_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)); "Invalid row type: " + positionToRowTypeMap.get(position)); } } } java/com/android/dialer/speeddial/SpeedDialCursor.javadeleted 100644 → 0 +0 −148 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.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; } } } } java/com/android/dialer/speeddial/SpeedDialFragment.java +33 −37 Original line number Diff line number Diff line Loading @@ -17,23 +17,28 @@ package com.android.dialer.speeddial; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.precall.PreCall; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import java.util.List; /** * Fragment for displaying: Loading @@ -47,13 +52,9 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen */ public class SpeedDialFragment extends Fragment { private static final int STREQUENT_CONTACTS_LOADER_ID = 1; private final SpeedDialHeaderListener headerListener = new SpeedDialFragmentHeaderListener(); private final FavoriteContactsListener favoritesListener = new SpeedDialFavoritesListener(); private final SuggestedContactsListener suggestedListener = new SpeedDialSuggestedListener(); private final SpeedDialFragmentLoaderCallback loaderCallback = new SpeedDialFragmentLoaderCallback(); private SpeedDialAdapter adapter; Loading @@ -72,7 +73,6 @@ public class SpeedDialFragment extends Fragment { new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener); recyclerView.setLayoutManager(adapter.getLayoutManager(getContext())); recyclerView.setAdapter(adapter); getLoaderManager().initLoader(STREQUENT_CONTACTS_LOADER_ID, null /* args */, loaderCallback); return view; } Loading @@ -84,7 +84,28 @@ public class SpeedDialFragment extends Fragment { @Override public void onResume() { super.onResume(); getLoaderManager().restartLoader(STREQUENT_CONTACTS_LOADER_ID, null, loaderCallback); Futures.addCallback( UiItemLoaderComponent.get(getContext().getApplicationContext()) .speedDialUiItemLoader() .loadSpeedDialUiItems(), new FutureCallback<List<SpeedDialUiItem>>() { @Override public void onSuccess(List<SpeedDialUiItem> speedDialUiItems) { // TODO(calderwoodra): this is bad new Handler(Looper.getMainLooper()) .post( () -> { adapter.setSpeedDialUiItems(speedDialUiItems); adapter.notifyDataSetChanged(); }); } @Override public void onFailure(Throwable throwable) { throw new RuntimeException(throwable); } }, MoreExecutors.directExecutor()); } private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener { Loading @@ -98,8 +119,8 @@ public class SpeedDialFragment extends Fragment { private class SpeedDialFavoritesListener implements FavoriteContactsListener { @Override public void onAmbiguousContactClicked(String lookupKey) { DisambigDialog.show(lookupKey, getFragmentManager()); public void onAmbiguousContactClicked(List<Channel> channels) { // TODO(calderwoodra): implement the disambig dialog with channels } @Override Loading Loading @@ -130,29 +151,4 @@ public class SpeedDialFragment extends Fragment { getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL)); } } /** * Loader callback that registers a content observer. {@link #unregisterContentObserver()} needs * to be called during tear down of the fragment. */ private class SpeedDialFragmentLoaderCallback implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { if (id == STREQUENT_CONTACTS_LOADER_ID) { return new StrequentContactsCursorLoader(getContext()); } throw Assert.createIllegalStateFailException("Invalid loader id: " + id); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { adapter.setCursor((SpeedDialCursor) data); } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.setCursor(null); } } } Loading
java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +2 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.android.dialer.precall.PreCallComponent; import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent; import com.android.dialer.simulator.SimulatorComponent; import com.android.dialer.spam.SpamComponent; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; import com.android.incallui.calllocation.CallLocationComponent; Loading Loading @@ -67,6 +68,7 @@ public interface BaseDialerRootComponent PhoneLookupDatabaseComponent.HasComponent, PhoneNumberGeoUtilComponent.HasComponent, PreCallComponent.HasComponent, UiItemLoaderComponent.HasComponent, SimSuggestionComponent.HasComponent, SimulatorComponent.HasComponent, SpamComponent.HasComponent, Loading
java/com/android/dialer/speeddial/FavoritesViewHolder.java +37 −45 Original line number Diff line number Diff line Loading @@ -17,13 +17,8 @@ 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; Loading @@ -31,8 +26,12 @@ 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; import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; import com.android.dialer.glidephotomanager.PhotoInfo; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.util.ArrayList; import java.util.List; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder Loading @@ -48,7 +47,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private boolean hasDefaultNumber; private boolean isVideoCall; private String number; private String lookupKey; private List<Channel> channels; public FavoritesViewHolder(View view, FavoriteContactsListener listener) { super(view); Loading @@ -62,44 +61,37 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder 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); public void bind(Context context, SpeedDialUiItem speedDialUiItem) { Assert.checkArgument(speedDialUiItem.isStarred()); String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID); 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)); nameView.setText(speedDialUiItem.name()); hasDefaultNumber = speedDialUiItem.defaultChannel() != null; if (hasDefaultNumber) { channels = new ArrayList<>(); isVideoCall = speedDialUiItem.defaultChannel().isVideoTechnology(); number = speedDialUiItem.defaultChannel().number(); phoneType.setText(speedDialUiItem.defaultChannel().label()); videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); // TODO(calderwoodra): Update this to include communication avenues also hasDefaultNumber = cursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0; } else { channels = speedDialUiItem.channels(); isVideoCall = false; number = null; phoneType.setText(""); videoCallIcon.setVisibility(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); GlidePhotoManagerComponent.get(context) .glidePhotoManager() .loadQuickContactBadge( photoView, PhotoInfo.newBuilder() .setPhotoId(speedDialUiItem.photoId()) .setPhotoUri(speedDialUiItem.photoUri()) .setName(speedDialUiItem.name()) .setLookupUri( Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey()) .toString()) .build()); } @Override Loading @@ -107,7 +99,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder if (hasDefaultNumber) { listener.onClick(number, isVideoCall); } else { listener.onAmbiguousContactClicked(lookupKey); listener.onAmbiguousContactClicked(channels); } } Loading @@ -122,7 +114,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder public interface FavoriteContactsListener { /** Called when the user clicks on a favorite contact that doesn't have a default number. */ void onAmbiguousContactClicked(String contactId); void onAmbiguousContactClicked(List<Channel> channels); /** Called when the user clicks on a favorite contact. */ void onClick(String number, boolean isVideoCall); Loading
java/com/android/dialer/speeddial/SpeedDialAdapter.java +84 −34 Original line number Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * Copyright (C) 2018 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. Loading @@ -16,19 +16,30 @@ package com.android.dialer.speeddial; import android.annotation.TargetApi; import android.content.Context; import android.os.Build.VERSION_CODES; import android.support.annotation.IntDef; import android.support.annotation.NonNull; 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.support.v7.widget.RecyclerView.ViewHolder; import android.util.ArrayMap; 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; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * RecyclerView adapter for {@link SpeedDialFragment}. Loading @@ -42,14 +53,26 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen * <li>Suggested contacts * </ol> */ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @SuppressWarnings("AndroidApiChecker") @TargetApi(VERSION_CODES.N) public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @Retention(RetentionPolicy.SOURCE) @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION}) @interface RowType { int STARRED_HEADER = 0; int SUGGESTION_HEADER = 1; int STARRED = 2; int SUGGESTION = 3; } private final Context context; private final FavoriteContactsListener favoritesListener; private final SuggestedContactsListener suggestedListener; private final SpeedDialHeaderListener headerListener; private SpeedDialCursor cursor; private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>(); private List<SpeedDialUiItem> speedDialUiItems; public SpeedDialAdapter( Context context, Loading @@ -64,39 +87,45 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @Override public int getItemViewType(int position) { return cursor.getRowType(position); return positionToRowTypeMap.get(position); } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(context); if (viewType == RowType.STARRED) { switch (viewType) { case RowType.STARRED: return new FavoritesViewHolder( inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener); } else if (viewType == RowType.SUGGESTION) { case RowType.SUGGESTION: return new SuggestionViewHolder( inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); } else if (viewType == RowType.HEADER) { case RowType.STARRED_HEADER: case RowType.SUGGESTION_HEADER: return new HeaderViewHolder( inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener); } else { default: 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; public void onBindViewHolder(@NonNull ViewHolder holder, int position) { switch (getItemViewType(position)) { case RowType.STARRED_HEADER: ((HeaderViewHolder) holder).setHeaderText(R.string.favorites_header); ((HeaderViewHolder) holder).showAddButton(true); return; case RowType.SUGGESTION_HEADER: ((HeaderViewHolder) holder).setHeaderText(R.string.suggestions_header); ((HeaderViewHolder) holder).showAddButton(false); return; case RowType.STARRED: ((FavoritesViewHolder) holder).bind(context, cursor); ((FavoritesViewHolder) holder).bind(context, speedDialUiItems.get(position - 1)); break; case RowType.SUGGESTION: ((SuggestionViewHolder) holder).bind(context, cursor); ((SuggestionViewHolder) holder).bind(context, speedDialUiItems.get(position - 2)); break; default: throw Assert.createIllegalStateFailException("Invalid view holder: " + holder); Loading @@ -105,15 +134,35 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @Override public int getItemCount() { return cursor == null || cursor.isClosed() ? 0 : cursor.getCount(); return positionToRowTypeMap.size(); } public void setCursor(SpeedDialCursor cursor) { this.cursor = cursor; notifyDataSetChanged(); public void setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems) { speedDialUiItems = new ArrayList<>(); speedDialUiItems.addAll(immutableSpeedDialUiItems); speedDialUiItems.sort((o1, o2) -> Boolean.compare(o2.isStarred(), o1.isStarred())); positionToRowTypeMap.clear(); if (speedDialUiItems.isEmpty()) { return; } // Show the add favorites even if there are no favorite contacts positionToRowTypeMap.put(0, RowType.STARRED_HEADER); int positionOfSuggestionHeader = 1; for (int i = 0; i < speedDialUiItems.size(); i++) { if (speedDialUiItems.get(i).isStarred()) { positionToRowTypeMap.put(i + 1, RowType.STARRED); // +1 for the header positionOfSuggestionHeader++; } else { positionToRowTypeMap.put(i + 2, RowType.SUGGESTION); // +2 for both headers } } if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) { positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER); } } LayoutManager getLayoutManager(Context context) { /* package-private */ LayoutManager getLayoutManager(Context context) { GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); layoutManager.setSpanSizeLookup( new SpanSizeLookup() { Loading @@ -127,15 +176,16 @@ final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde @VisibleForTesting int getSpanSize(int position) { switch (cursor.getRowType(position)) { switch (getItemViewType(position)) { case RowType.SUGGESTION: case RowType.HEADER: case RowType.STARRED_HEADER: case RowType.SUGGESTION_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)); "Invalid row type: " + positionToRowTypeMap.get(position)); } } }
java/com/android/dialer/speeddial/SpeedDialCursor.javadeleted 100644 → 0 +0 −148 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.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; } } } }
java/com/android/dialer/speeddial/SpeedDialFragment.java +33 −37 Original line number Diff line number Diff line Loading @@ -17,23 +17,28 @@ package com.android.dialer.speeddial; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.precall.PreCall; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import java.util.List; /** * Fragment for displaying: Loading @@ -47,13 +52,9 @@ import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListen */ public class SpeedDialFragment extends Fragment { private static final int STREQUENT_CONTACTS_LOADER_ID = 1; private final SpeedDialHeaderListener headerListener = new SpeedDialFragmentHeaderListener(); private final FavoriteContactsListener favoritesListener = new SpeedDialFavoritesListener(); private final SuggestedContactsListener suggestedListener = new SpeedDialSuggestedListener(); private final SpeedDialFragmentLoaderCallback loaderCallback = new SpeedDialFragmentLoaderCallback(); private SpeedDialAdapter adapter; Loading @@ -72,7 +73,6 @@ public class SpeedDialFragment extends Fragment { new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener); recyclerView.setLayoutManager(adapter.getLayoutManager(getContext())); recyclerView.setAdapter(adapter); getLoaderManager().initLoader(STREQUENT_CONTACTS_LOADER_ID, null /* args */, loaderCallback); return view; } Loading @@ -84,7 +84,28 @@ public class SpeedDialFragment extends Fragment { @Override public void onResume() { super.onResume(); getLoaderManager().restartLoader(STREQUENT_CONTACTS_LOADER_ID, null, loaderCallback); Futures.addCallback( UiItemLoaderComponent.get(getContext().getApplicationContext()) .speedDialUiItemLoader() .loadSpeedDialUiItems(), new FutureCallback<List<SpeedDialUiItem>>() { @Override public void onSuccess(List<SpeedDialUiItem> speedDialUiItems) { // TODO(calderwoodra): this is bad new Handler(Looper.getMainLooper()) .post( () -> { adapter.setSpeedDialUiItems(speedDialUiItems); adapter.notifyDataSetChanged(); }); } @Override public void onFailure(Throwable throwable) { throw new RuntimeException(throwable); } }, MoreExecutors.directExecutor()); } private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener { Loading @@ -98,8 +119,8 @@ public class SpeedDialFragment extends Fragment { private class SpeedDialFavoritesListener implements FavoriteContactsListener { @Override public void onAmbiguousContactClicked(String lookupKey) { DisambigDialog.show(lookupKey, getFragmentManager()); public void onAmbiguousContactClicked(List<Channel> channels) { // TODO(calderwoodra): implement the disambig dialog with channels } @Override Loading Loading @@ -130,29 +151,4 @@ public class SpeedDialFragment extends Fragment { getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL)); } } /** * Loader callback that registers a content observer. {@link #unregisterContentObserver()} needs * to be called during tear down of the fragment. */ private class SpeedDialFragmentLoaderCallback implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { if (id == STREQUENT_CONTACTS_LOADER_ID) { return new StrequentContactsCursorLoader(getContext()); } throw Assert.createIllegalStateFailException("Invalid loader id: " + id); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { adapter.setCursor((SpeedDialCursor) data); } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.setCursor(null); } } }