From 9873647e903574ee4ef62b2f13633650793c346e Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Thu, 30 Nov 2017 15:21:47 -0800 Subject: Implemented new favorites list UI. Bug: 36841782 Test: implemented PiperOrigin-RevId: 177516412 Change-Id: If9478ce22c10fd17e352d5fdcc2c0bef5e14a6d8 --- java/com/android/dialer/app/DialtactsActivity.java | 11 +- .../dialer/speeddial/FavoritesViewHolder.java | 121 ++++++++++++++++ .../android/dialer/speeddial/HeaderViewHolder.java | 60 ++++++++ .../android/dialer/speeddial/SpeedDialAdapter.java | 141 +++++++++++++++++++ .../android/dialer/speeddial/SpeedDialCursor.java | 152 +++++++++++++++++++++ .../dialer/speeddial/SpeedDialFragment.java | 119 +++++++++++++++- .../android/dialer/speeddial/SquareImageView.java | 36 +++++ .../speeddial/StrequentContactsCursorLoader.java | 104 ++++++++++++++ .../dialer/speeddial/SuggestionViewHolder.java | 117 ++++++++++++++++ .../speeddial/res/drawable/favorite_icon.xml | 23 ++++ .../speeddial/res/layout/favorite_item_layout.xml | 70 ++++++++++ .../speeddial/res/layout/fragment_speed_dial.xml | 12 +- .../res/layout/speed_dial_header_layout.xml | 41 ++++++ .../speeddial/res/layout/suggestion_row_layout.xml | 61 +++++++++ .../dialer/speeddial/res/values/strings.xml | 8 ++ .../com/android/dialer/theme/res/values/dimens.xml | 9 ++ 16 files changed, 1079 insertions(+), 6 deletions(-) create mode 100644 java/com/android/dialer/speeddial/FavoritesViewHolder.java create mode 100644 java/com/android/dialer/speeddial/HeaderViewHolder.java create mode 100644 java/com/android/dialer/speeddial/SpeedDialAdapter.java create mode 100644 java/com/android/dialer/speeddial/SpeedDialCursor.java create mode 100644 java/com/android/dialer/speeddial/SquareImageView.java create mode 100644 java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java create mode 100644 java/com/android/dialer/speeddial/SuggestionViewHolder.java create mode 100644 java/com/android/dialer/speeddial/res/drawable/favorite_icon.xml create mode 100644 java/com/android/dialer/speeddial/res/layout/favorite_item_layout.xml create mode 100644 java/com/android/dialer/speeddial/res/layout/speed_dial_header_layout.xml create mode 100644 java/com/android/dialer/speeddial/res/layout/suggestion_row_layout.xml (limited to 'java/com/android/dialer') diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index 9144fc939..2d82b6f33 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -1555,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. @@ -1615,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; @@ -1746,4 +1751,8 @@ public class DialtactsActivity extends TransactionSafeActivity static void setVoiceSearchEnabledForTest(Optional enabled) { sVoiceSearchEnabledForTest = enabled; } + + private boolean newFavoritesIsEnabled() { + return ConfigProviderBindings.get(this).getBoolean("enable_new_favorites_tab", false); + } } diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java new file mode 100644 index 000000000..0cde71693 --- /dev/null +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -0,0 +1,121 @@ +/* + * 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); + } +} diff --git a/java/com/android/dialer/speeddial/HeaderViewHolder.java b/java/com/android/dialer/speeddial/HeaderViewHolder.java new file mode 100644 index 000000000..58120eed7 --- /dev/null +++ b/java/com/android/dialer/speeddial/HeaderViewHolder.java @@ -0,0 +1,60 @@ +/* + * 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(); + } +} diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java new file mode 100644 index 000000000..5f7b68e5c --- /dev/null +++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java @@ -0,0 +1,141 @@ +/* + * 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}. + * + *

Displays a list in the following order: + * + *

    + *
  1. Favorite contacts header (with add button) + *
  2. Favorite contacts + *
  3. Suggested contacts header + *
  4. Suggested contacts + *
+ */ +final class SpeedDialAdapter extends RecyclerView.Adapter { + + 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)); + } + } +} diff --git a/java/com/android/dialer/speeddial/SpeedDialCursor.java b/java/com/android/dialer/speeddial/SpeedDialCursor.java new file mode 100644 index 000000000..552fab175 --- /dev/null +++ b/java/com/android/dialer/speeddial/SpeedDialCursor.java @@ -0,0 +1,152 @@ +/* + * 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 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; + } + } + } +} diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index c087439eb..65e542cd4 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -17,15 +17,37 @@ package com.android.dialer.speeddial; import android.app.Fragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Loader; +import android.database.Cursor; import android.os.Bundle; +import android.provider.ContactsContract.Contacts; import android.support.annotation.Nullable; +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; /** Favorites fragment. Contents TBD. TODO(calderwoodra) */ 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; + public static SpeedDialFragment newInstance() { return new SpeedDialFragment(); } @@ -34,11 +56,106 @@ public class SpeedDialFragment extends Fragment { @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_speed_dial, container, false); + View view = inflater.inflate(R.layout.fragment_speed_dial, container, false); + RecyclerView recyclerView = view.findViewById(R.id.speed_dial_recycler_view); + + adapter = + 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; } public boolean hasFrequents() { // TODO(calderwoodra) return false; } + + @Override + public void onPause() { + super.onPause(); + loaderCallback.unregisterContentObserver(); + } + + private static class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener { + + @Override + public void onAddFavoriteClicked() { + // TODO(calderwoodra): implement add favorite screen + } + } + + private class SpeedDialFavoritesListener implements FavoriteContactsListener { + + @Override + public void onClick(String number, boolean isVideoCall) { + // TODO(calderwoodra): add logic for duo video calls + PreCall.start( + getContext(), + new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL) + .setIsVideoCall(isVideoCall)); + } + + @Override + public void onLongClick(String number) { + // TODO(calderwoodra): show favorite contact floating context menu + } + } + + private class SpeedDialSuggestedListener implements SuggestedContactsListener { + + @Override + public void onOverFlowMenuClicked(String number) { + // TODO(calderwoodra) show overflow menu for suggested contacts + } + + @Override + public void onRowClicked(String number) { + PreCall.start( + 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 { + + private StrequentContactsCursorLoader cursorLoader; + + @Override + public Loader 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 loader, Cursor data) { + cursorLoader = (StrequentContactsCursorLoader) loader; + // Since the original cursor we queried against was modified and closed, we need to register a + // new content observer in order to get updates on changes to our contacts. + getContext() + .getContentResolver() + .registerContentObserver( + Contacts.CONTENT_STREQUENT_URI, + true /* notifyForDescendants*/, + cursorLoader.getContentObserver()); + adapter.setCursor((SpeedDialCursor) data); + } + + public void unregisterContentObserver() { + getContext() + .getContentResolver() + .unregisterContentObserver(cursorLoader.getContentObserver()); + } + + @Override + public void onLoaderReset(Loader loader) { + adapter.setCursor(null); + } + } } diff --git a/java/com/android/dialer/speeddial/SquareImageView.java b/java/com/android/dialer/speeddial/SquareImageView.java new file mode 100644 index 000000000..a12f4d426 --- /dev/null +++ b/java/com/android/dialer/speeddial/SquareImageView.java @@ -0,0 +1,36 @@ +/* + * 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.Nullable; +import android.util.AttributeSet; +import android.widget.QuickContactBadge; + +/** A square {@link android.widget.ImageView} constrained on width. */ +public class SquareImageView extends QuickContactBadge { + + public SquareImageView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setClickable(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java new file mode 100644 index 000000000..f5f0045e0 --- /dev/null +++ b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java @@ -0,0 +1,104 @@ +/* + * 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.CursorLoader; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; + +/** Cursor Loader for strequent contacts. */ +final class StrequentContactsCursorLoader extends CursorLoader { + + static final int PHONE_ID = 0; + static final int PHONE_DISPLAY_NAME = 1; + static final int PHONE_STARRED = 2; + static final int PHONE_PHOTO_URI = 3; + static final int PHONE_LOOKUP_KEY = 4; + static final int PHONE_PHOTO_ID = 5; + static final int PHONE_NUMBER = 6; + static final int PHONE_TYPE = 7; + static final int PHONE_LABEL = 8; + static final int PHONE_IS_SUPER_PRIMARY = 9; + static final int PHONE_PINNED = 10; + static final int PHONE_CONTACT_ID = 11; + + static final String[] PHONE_PROJECTION = + new String[] { + Phone._ID, // 0 + Phone.DISPLAY_NAME, // 1 + Phone.STARRED, // 2 + Phone.PHOTO_URI, // 3 + Phone.LOOKUP_KEY, // 4 + Phone.PHOTO_ID, // 5 + Phone.NUMBER, // 6 + Phone.TYPE, // 7 + Phone.LABEL, // 8 + Phone.IS_SUPER_PRIMARY, // 9 + Phone.PINNED, // 10 + Phone.CONTACT_ID, // 11 + }; + + private final ContentObserver contentObserver = new ForceLoadContentObserver(); + + StrequentContactsCursorLoader(Context context) { + super( + context, + buildUri(), + PHONE_PROJECTION, + null /* selection */, + null /* selectionArgs */, + null /* sortOrder */); + // TODO(calderwoodra): implement alternative display names + } + + static void addToCursor(MatrixCursor dest, Cursor source) { + dest.newRow() + .add(PHONE_PROJECTION[PHONE_ID], source.getLong(PHONE_ID)) + .add(PHONE_PROJECTION[PHONE_DISPLAY_NAME], source.getString(PHONE_DISPLAY_NAME)) + .add(PHONE_PROJECTION[PHONE_STARRED], source.getInt(PHONE_STARRED)) + .add(PHONE_PROJECTION[PHONE_PHOTO_URI], source.getString(PHONE_PHOTO_URI)) + .add(PHONE_PROJECTION[PHONE_LOOKUP_KEY], source.getString(PHONE_LOOKUP_KEY)) + .add(PHONE_PROJECTION[PHONE_NUMBER], source.getString(PHONE_NUMBER)) + .add(PHONE_PROJECTION[PHONE_TYPE], source.getInt(PHONE_TYPE)) + .add(PHONE_PROJECTION[PHONE_LABEL], source.getString(PHONE_LABEL)) + .add(PHONE_PROJECTION[PHONE_IS_SUPER_PRIMARY], source.getInt(PHONE_IS_SUPER_PRIMARY)) + .add(PHONE_PROJECTION[PHONE_PINNED], source.getInt(PHONE_PINNED)) + .add(PHONE_PROJECTION[PHONE_CONTACT_ID], source.getLong(PHONE_CONTACT_ID)); + } + + private static Uri buildUri() { + return Contacts.CONTENT_STREQUENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") + .build(); + } + + @Override + public Cursor loadInBackground() { + return SpeedDialCursor.newInstance(super.loadInBackground()); + } + + ContentObserver getContentObserver() { + return contentObserver; + } +} diff --git a/java/com/android/dialer/speeddial/SuggestionViewHolder.java b/java/com/android/dialer/speeddial/SuggestionViewHolder.java new file mode 100644 index 000000000..70df30706 --- /dev/null +++ b/java/com/android/dialer/speeddial/SuggestionViewHolder.java @@ -0,0 +1,117 @@ +/* + * 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.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.dialer.contactphoto.ContactPhotoManager; +import com.android.dialer.lettertile.LetterTileDrawable; +import com.android.dialer.location.GeoUtil; + +/** ViewHolder for displaying suggested contacts in {@link SpeedDialFragment}. */ +public class SuggestionViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + private final SuggestedContactsListener listener; + + private final QuickContactBadge photoView; + private final TextView nameOrNumberView; + private final TextView numberView; + + private String number; + + SuggestionViewHolder(View view, SuggestedContactsListener listener) { + super(view); + photoView = view.findViewById(R.id.avatar); + nameOrNumberView = view.findViewById(R.id.name); + numberView = view.findViewById(R.id.number); + itemView.setOnClickListener(this); + view.findViewById(R.id.overflow).setOnClickListener(this); + this.listener = listener; + } + + public void bind(Context context, Cursor cursor) { + number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER); + number = PhoneNumberUtils.formatNumber(number, GeoUtil.getCurrentCountryIso(context)); + + String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME); + String label = getLabel(context.getResources(), cursor); + String secondaryInfo = + TextUtils.isEmpty(label) + ? number + : context.getString( + com.android.contacts.common.R.string.call_subject_type_and_number, label, number); + + nameOrNumberView.setText(name); + numberView.setText(secondaryInfo); + + 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); + } + + // 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) { + if (v.getId() == R.id.overflow) { + listener.onOverFlowMenuClicked(number); + } else { + listener.onRowClicked(number); + } + } + + /** Listener/Callback for {@link SuggestionViewHolder} parents. */ + public interface SuggestedContactsListener { + + void onOverFlowMenuClicked(String number); + + /** Called when a suggested contact is clicked. */ + void onRowClicked(String number); + } +} diff --git a/java/com/android/dialer/speeddial/res/drawable/favorite_icon.xml b/java/com/android/dialer/speeddial/res/drawable/favorite_icon.xml new file mode 100644 index 000000000..81b018ff1 --- /dev/null +++ b/java/com/android/dialer/speeddial/res/drawable/favorite_icon.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/layout/favorite_item_layout.xml b/java/com/android/dialer/speeddial/res/layout/favorite_item_layout.xml new file mode 100644 index 000000000..fb476659c --- /dev/null +++ b/java/com/android/dialer/speeddial/res/layout/favorite_item_layout.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml index 04e230e4d..d432f097b 100644 --- a/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml +++ b/java/com/android/dialer/speeddial/res/layout/fragment_speed_dial.xml @@ -14,7 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> - + diff --git a/java/com/android/dialer/speeddial/res/layout/speed_dial_header_layout.xml b/java/com/android/dialer/speeddial/res/layout/speed_dial_header_layout.xml new file mode 100644 index 000000000..0a84b41e6 --- /dev/null +++ b/java/com/android/dialer/speeddial/res/layout/speed_dial_header_layout.xml @@ -0,0 +1,41 @@ + + + + + + +