From af3cc176cdc652a05645cab9ad213696970c6e4d Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Fri, 1 Sep 2017 23:28:21 -0700 Subject: Added search actions to the end of the dialpad search results. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now create new contacts, add to existing contacts, send sms and make ViLTE calls from dialpad search results. screenshot: http://screen/7iR038nUvmh from the bugbash: 11. Missing “Create new contact” “Add to a contact” “Send SMS” from search results with a phone number search Bug: 64902476 Test: many PiperOrigin-RevId: 167362073 Change-Id: I2f94d863035c119ec526e02e088992c618a858a9 --- java/com/android/dialer/app/DialtactsActivity.java | 8 +- .../searchfragment/list/NewSearchFragment.java | 40 ++++-- .../list/SearchActionViewHolder.java | 145 +++++++++++++++++++++ .../dialer/searchfragment/list/SearchAdapter.java | 14 ++ .../searchfragment/list/SearchCursorManager.java | 35 ++++- .../list/res/layout/search_action_layout.xml | 39 ++++++ .../searchfragment/list/res/values/strings.xml | 14 ++ 7 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 java/com/android/dialer/searchfragment/list/SearchActionViewHolder.java create mode 100644 java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java index fc557f0b6..7f5a9b94a 100644 --- a/java/com/android/dialer/app/DialtactsActivity.java +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -309,10 +309,7 @@ public class DialtactsActivity extends TransactionSafeActivity } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { mRegularSearchFragment.setQueryString(mSearchQuery); } else if (mNewSearchFragment != null && mNewSearchFragment.isVisible()) { - mNewSearchFragment.setQuery(mSearchQuery); - // When the user switches between dialpad and the serachbar, we need to reset the - // call initiation type. - mNewSearchFragment.setCallInitiationType(getCallInitiationType()); + mNewSearchFragment.setQuery(mSearchQuery, getCallInitiationType()); } } @@ -1211,8 +1208,7 @@ public class DialtactsActivity extends TransactionSafeActivity if (!smartDialSearch && !useNewSearch) { ((SearchFragment) fragment).setQueryString(query); } else if (useNewSearch) { - ((NewSearchFragment) fragment).setQuery(query); - ((NewSearchFragment) fragment).setCallInitiationType(getCallInitiationType()); + ((NewSearchFragment) fragment).setQuery(query, getCallInitiationType()); } transaction.commit(); diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java index 5b3532cdb..0623d394a 100644 --- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java +++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java @@ -28,6 +28,7 @@ import android.support.annotation.VisibleForTesting; import android.support.v13.app.FragmentCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -44,16 +45,19 @@ import com.android.dialer.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener; import com.android.dialer.searchfragment.common.SearchCursor; import com.android.dialer.searchfragment.cp2.SearchContactsCursorLoader; +import com.android.dialer.searchfragment.list.SearchActionViewHolder.Action; import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader; import com.android.dialer.searchfragment.remote.RemoteContactsCursorLoader; import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader; import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader.Directory; +import com.android.dialer.util.CallUtil; import com.android.dialer.util.PermissionsUtil; import com.android.dialer.util.ViewUtil; import com.android.dialer.widget.EmptyContentView; import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** Fragment used for searching contacts. */ @@ -99,6 +103,7 @@ public final class NewSearchFragment extends Fragment View view = inflater.inflate(R.layout.fragment_search, parent, false); adapter = new SearchAdapter(getActivity(), new SearchCursorManager()); adapter.setCallInitiationType(callInitiationType); + adapter.setSearchActions(getActions()); emptyContentView = view.findViewById(R.id.empty_view); recyclerView = view.findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -184,22 +189,18 @@ public final class NewSearchFragment extends Fragment recyclerView.setAdapter(null); } - public void setQuery(String query) { + public void setQuery(String query, CallInitiationType.Type callInitiationType) { this.query = query; + this.callInitiationType = callInitiationType; if (adapter != null) { adapter.setQuery(query); + adapter.setCallInitiationType(callInitiationType); + adapter.setSearchActions(getActions()); loadNearbyPlacesCursor(); loadRemoteContactsCursors(); } } - public void setCallInitiationType(CallInitiationType.Type callInitiationType) { - this.callInitiationType = callInitiationType; - if (adapter != null) { - adapter.setCallInitiationType(callInitiationType); - } - } - /** Translate the search fragment and resize it to fit on the screen. */ public void animatePosition(int start, int end, int duration) { // Called before the view is ready, prepare a runnable to run in onCreateView @@ -329,4 +330,27 @@ public final class NewSearchFragment extends Fragment public void setRemoteDirectoriesDisabled(boolean disabled) { remoteDirectoriesDisabledForTesting = disabled; } + + /** + * Returns a list of search actions to be shown in the search results. + * + *

List will be empty if query is 1 or 0 characters or the query isn't from the Dialpad. For + * the list of supported actions, see {@link SearchActionViewHolder.Action}. + */ + private List getActions() { + if (TextUtils.isEmpty(query) + || query.length() == 1 + || callInitiationType == CallInitiationType.Type.REGULAR_SEARCH) { + return Collections.emptyList(); + } + + List actions = new ArrayList<>(); + actions.add(Action.CREATE_NEW_CONTACT); + actions.add(Action.ADD_TO_CONTACT); + actions.add(Action.SEND_SMS); + if (CallUtil.isVideoEnabled(getContext())) { + actions.add(Action.MAKE_VILTE_CALL); + } + return actions; + } } diff --git a/java/com/android/dialer/searchfragment/list/SearchActionViewHolder.java b/java/com/android/dialer/searchfragment/list/SearchActionViewHolder.java new file mode 100644 index 000000000..62e5c72b0 --- /dev/null +++ b/java/com/android/dialer/searchfragment/list/SearchActionViewHolder.java @@ -0,0 +1,145 @@ +/* + * 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.searchfragment.list; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.IntDef; +import android.support.annotation.StringRes; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.callintent.CallInitiationType; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.logging.DialerImpression; +import com.android.dialer.logging.Logger; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * {@link RecyclerView.ViewHolder} for showing an {@link SearchActionViewHolder.Action} in a list. + */ +final class SearchActionViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + /** IntDef for the different types of actions that can be used. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Action.INVALID, + Action.CREATE_NEW_CONTACT, + Action.ADD_TO_CONTACT, + Action.SEND_SMS, + Action.MAKE_VILTE_CALL + }) + @interface Action { + int INVALID = 0; + /** Opens the prompt to create a new contact. */ + int CREATE_NEW_CONTACT = 1; + /** Opens a prompt to add to an existing contact. */ + int ADD_TO_CONTACT = 2; + /** Opens the SMS conversation with the default SMS app. */ + int SEND_SMS = 3; + /** Attempts to make a VILTE call to the number. */ + int MAKE_VILTE_CALL = 4; + } + + private final Context context; + private final ImageView actionImage; + private final TextView actionText; + + private @Action int action; + private int position; + private String query; + + SearchActionViewHolder(View view) { + super(view); + context = view.getContext(); + actionImage = view.findViewById(R.id.search_action_image); + actionText = view.findViewById(R.id.search_action_text); + view.setOnClickListener(this); + } + + void setAction(@Action int action, int position, String query) { + this.action = action; + this.position = position; + this.query = query; + switch (action) { + case Action.ADD_TO_CONTACT: + actionText.setText(R.string.search_shortcut_add_to_contact); + actionImage.setImageResource(R.drawable.quantum_ic_person_add_vd_theme_24); + break; + case Action.CREATE_NEW_CONTACT: + actionText.setText(R.string.search_shortcut_create_new_contact); + actionImage.setImageResource(R.drawable.quantum_ic_person_add_vd_theme_24); + break; + case Action.MAKE_VILTE_CALL: + actionText.setText(R.string.search_shortcut_make_video_call); + actionImage.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); + break; + case Action.SEND_SMS: + actionText.setText(R.string.search_shortcut_send_sms_message); + actionImage.setImageResource(R.drawable.quantum_ic_message_vd_theme_24); + break; + case Action.INVALID: + default: + throw Assert.createIllegalStateFailException("Invalid action: " + action); + } + } + + @Override + public void onClick(View v) { + switch (action) { + case Action.ADD_TO_CONTACT: + Logger.get(context).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_DIALPAD); + Intent intent = IntentUtil.getAddToExistingContactIntent(query); + @StringRes int errorString = R.string.add_contact_not_available; + DialerUtils.startActivityWithErrorToast(context, intent, errorString); + break; + + case Action.CREATE_NEW_CONTACT: + Logger.get(context).logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_DIALPAD); + intent = IntentUtil.getNewContactIntent(query); + DialerUtils.startActivityWithErrorToast(context, intent); + break; + + case Action.MAKE_VILTE_CALL: + CallSpecificAppData callSpecificAppData = + CallSpecificAppData.newBuilder() + .setCallInitiationType(CallInitiationType.Type.DIALPAD) + .setPositionOfSelectedSearchResult(position) + .setCharactersInSearchString(query.length()) + .build(); + intent = new CallIntentBuilder(query, callSpecificAppData).setIsVideoCall(true).build(); + DialerUtils.startActivityWithErrorToast(context, intent); + break; + + case Action.SEND_SMS: + intent = IntentUtil.getSendSmsIntent(query); + DialerUtils.startActivityWithErrorToast(context, intent); + break; + + case Action.INVALID: + default: + throw Assert.createIllegalStateFailException("Invalid action: " + action); + } + } +} diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java index f08d60e09..61055a0c1 100644 --- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java +++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java @@ -40,6 +40,7 @@ import com.android.dialer.searchfragment.list.SearchCursorManager.RowType; import com.android.dialer.searchfragment.nearbyplaces.NearbyPlaceViewHolder; import com.android.dialer.searchfragment.remote.RemoteContactViewHolder; import com.android.dialer.util.DialerUtils; +import java.util.List; /** RecyclerView adapter for {@link NewSearchFragment}. */ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) @@ -75,6 +76,9 @@ public final class SearchAdapter extends RecyclerView.Adapter case RowType.DIRECTORY_ROW: return new RemoteContactViewHolder( LayoutInflater.from(activity).inflate(R.layout.search_contact_row, root, false)); + case RowType.SEARCH_ACTION: + return new SearchActionViewHolder( + LayoutInflater.from(activity).inflate(R.layout.search_action_layout, root, false)); case RowType.INVALID: default: throw Assert.createIllegalStateFailException("Invalid RowType: " + rowType); @@ -98,6 +102,9 @@ public final class SearchAdapter extends RecyclerView.Adapter String header = searchCursorManager.getCursor(position).getString(SearchCursor.HEADER_TEXT_POSITION); ((HeaderViewHolder) holder).setHeader(header); + } else if (holder instanceof SearchActionViewHolder) { + ((SearchActionViewHolder) holder) + .setAction(searchCursorManager.getSearchAction(position), position, query); } else { throw Assert.createIllegalStateFailException("Invalid ViewHolder: " + holder); } @@ -124,6 +131,13 @@ public final class SearchAdapter extends RecyclerView.Adapter } } + /** Sets the actions to be shown at the bottom of the search results. */ + void setSearchActions(List actions) { + if (searchCursorManager.setSearchActions(actions)) { + notifyDataSetChanged(); + } + } + void setCallInitiationType(CallInitiationType.Type callInitiationType) { this.callInitiationType = callInitiationType; } diff --git a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java index a303425d3..95bede001 100644 --- a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java +++ b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java @@ -23,16 +23,19 @@ import com.android.dialer.common.Assert; import com.android.dialer.searchfragment.common.SearchCursor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; /** * Manages all of the cursors needed for {@link SearchAdapter}. * - *

This class accepts three cursors: + *

This class accepts four data sources: * *

* *

The key purpose of this class is to compose three aforementioned cursors together to function @@ -56,7 +59,8 @@ public final class SearchCursorManager { SearchCursorManager.RowType.NEARBY_PLACES_HEADER, SearchCursorManager.RowType.NEARBY_PLACES_ROW, SearchCursorManager.RowType.DIRECTORY_HEADER, - SearchCursorManager.RowType.DIRECTORY_ROW + SearchCursorManager.RowType.DIRECTORY_ROW, + SearchCursorManager.RowType.SEARCH_ACTION }) @interface RowType { int INVALID = 0; @@ -73,11 +77,14 @@ public final class SearchCursorManager { int DIRECTORY_HEADER = 5; /** A row containing contact information for contacts stored externally in corp directories. */ int DIRECTORY_ROW = 6; + /** A row containing a search action */ + int SEARCH_ACTION = 7; } private SearchCursor contactsCursor = null; private SearchCursor nearbyPlacesCursor = null; private SearchCursor corpDirectoryCursor = null; + private List searchActions = new ArrayList<>(); /** Returns true if the cursor changed. */ boolean setContactsCursor(@Nullable SearchCursor cursor) { @@ -149,6 +156,20 @@ public final class SearchCursorManager { return updated; } + /** Sets search actions, returning true if different from existing actions. */ + boolean setSearchActions(List searchActions) { + if (!this.searchActions.equals(searchActions)) { + this.searchActions = searchActions; + return true; + } + return false; + } + + /** Returns {@link SearchActionViewHolder.Action}. */ + int getSearchAction(int position) { + return searchActions.get(position - getCount() + searchActions.size()); + } + /** Returns the sum of counts of all cursors, including headers. */ int getCount() { int count = 0; @@ -164,11 +185,19 @@ public final class SearchCursorManager { count += corpDirectoryCursor.getCount(); } - return count; + return count + searchActions.size(); } @RowType int getRowType(int position) { + int cursorCount = getCount(); + if (position >= cursorCount) { + throw Assert.createIllegalStateFailException( + String.format("Invalid position: %d, cursor count: %d", position, cursorCount)); + } else if (position >= cursorCount - searchActions.size()) { + return RowType.SEARCH_ACTION; + } + SearchCursor cursor = getCursor(position); if (cursor == contactsCursor) { return cursor.isHeader() ? RowType.CONTACT_HEADER : RowType.CONTACT_ROW; diff --git a/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml new file mode 100644 index 000000000..99d0fbf0c --- /dev/null +++ b/java/com/android/dialer/searchfragment/list/res/layout/search_action_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/searchfragment/list/res/values/strings.xml b/java/com/android/dialer/searchfragment/list/res/values/strings.xml index 0d25b8c7a..ea238fc92 100644 --- a/java/com/android/dialer/searchfragment/list/res/values/strings.xml +++ b/java/com/android/dialer/searchfragment/list/res/values/strings.xml @@ -17,4 +17,18 @@ To search your contacts, turn on the Contacts permissions. + + + Create new contact + + + Add to a contact + + + Send SMS + + + Make video call -- cgit v1.2.3