diff --git a/res/drawable/no_search_results.xml b/res/drawable/no_search_results.xml new file mode 100644 index 00000000000..a75a4438152 --- /dev/null +++ b/res/drawable/no_search_results.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/search_feedback.xml b/res/layout/search_feedback.xml new file mode 100644 index 00000000000..cdb0545d8a4 --- /dev/null +++ b/res/layout/search_feedback.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/res/layout/search_panel_2.xml b/res/layout/search_panel_2.xml index 671c19c0bdc..8f7284721c5 100644 --- a/res/layout/search_panel_2.xml +++ b/res/layout/search_panel_2.xml @@ -13,18 +13,19 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + android:orientation="vertical" + android:layout_alignParentTop="true"> + android:scrollbars="vertical"/> + + + + + + + - \ No newline at end of file + + diff --git a/res/values/strings.xml b/res/values/strings.xml index d2e39154a1b..4ef62160c3a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2167,6 +2167,8 @@ Search Manage search settings and history + + No results diff --git a/src/com/android/settings/search2/SearchActivity.java b/src/com/android/settings/search2/SearchActivity.java index 25a54cfcf47..5a8455b68c7 100644 --- a/src/com/android/settings/search2/SearchActivity.java +++ b/src/com/android/settings/search2/SearchActivity.java @@ -21,6 +21,7 @@ import android.app.Fragment; import android.app.FragmentManager; import android.os.Bundle; +import android.view.WindowManager; import com.android.settings.R; public class SearchActivity extends Activity { @@ -29,6 +30,8 @@ public class SearchActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.search_main); + // Keeps layouts in-place when keyboard opens. + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); FragmentManager fragmentManager = getFragmentManager(); Fragment fragment = fragmentManager.findFragmentById(R.id.main_content); diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java index a9be5a15a87..d3dc24b026f 100644 --- a/src/com/android/settings/search2/SearchFeatureProvider.java +++ b/src/com/android/settings/search2/SearchFeatureProvider.java @@ -19,6 +19,7 @@ import android.app.Activity; import android.content.Context; import android.view.Menu; +import android.view.View; import com.android.settings.dashboard.SiteMapManager; /** @@ -68,4 +69,25 @@ public interface SearchFeatureProvider { * Updates the Settings indexes */ void updateIndex(Context context); + + /** + * Initializes the feedback button in case it was dismissed. + */ + default void initFeedbackButton() { + } + + /** + * Show a button users can click to submit feedback on the quality of the search results. + */ + default void showFeedbackButton(SearchFragment fragment, View view) { + } + + /** + * Hide the feedback button shown by + * {@link #showFeedbackButton(SearchFragment fragment, View view) showFeedbackButton} + */ + default void hideFeedbackButton() { + } + + } diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java index 24f7015a1e7..957713bd6b0 100644 --- a/src/com/android/settings/search2/SearchFragment.java +++ b/src/com/android/settings/search2/SearchFragment.java @@ -31,6 +31,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.SearchView; @@ -71,7 +72,7 @@ public class SearchFragment extends InstrumentedFragment implements @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) static final String RESULT_CLICK_COUNT = "settings_search_result_click_count"; - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting String mQuery; private final SaveQueryRecorderCallback mSaveQueryRecorderCallback = @@ -89,6 +90,7 @@ public class SearchFragment extends InstrumentedFragment implements @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) RecyclerView mResultsRecyclerView; private SearchView mSearchView; + private LinearLayout mNoResultsView; @VisibleForTesting final RecyclerView.OnScrollListener mScrollListener = @@ -118,6 +120,8 @@ public class SearchFragment extends InstrumentedFragment implements super.onCreate(savedInstanceState); setHasOptionsMenu(true); mSearchAdapter = new SearchResultsAdapter(this); + + mSearchFeatureProvider.initFeedbackButton(); final LoaderManager loaderManager = getLoaderManager(); @@ -155,6 +159,8 @@ public class SearchFragment extends InstrumentedFragment implements mResultsRecyclerView.setAdapter(mSearchAdapter); mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); mResultsRecyclerView.addOnScrollListener(mScrollListener); + + mNoResultsView = (LinearLayout) view.findViewById(R.id.no_results_layout); return view; } @@ -184,16 +190,26 @@ public class SearchFragment extends InstrumentedFragment implements if (TextUtils.equals(query, mQuery)) { return true; } + + final boolean isEmptyQuery = TextUtils.isEmpty(query); + + // Hide no-results-view when the new query is not a super-string of the previous + if ((mQuery != null) && (mNoResultsView.getVisibility() == View.VISIBLE) + && (query.length() < mQuery.length())) { + mNoResultsView.setVisibility(View.GONE); + } + mResultClickCount = 0; mNeverEnteredQuery = false; mQuery = query; mSearchAdapter.clearResults(); - if (TextUtils.isEmpty(mQuery)) { + if (isEmptyQuery) { final LoaderManager loaderManager = getLoaderManager(); loaderManager.destroyLoader(LOADER_ID_DATABASE); loaderManager.destroyLoader(LOADER_ID_INSTALLED_APPS); loaderManager.restartLoader(LOADER_ID_RECENTS, null /* args */, this /* callback */); + mSearchFeatureProvider.hideFeedbackButton(); } else { restartLoaders(); } @@ -232,7 +248,12 @@ public class SearchFragment extends InstrumentedFragment implements mSearchAdapter.addResultsToMap(data, loader.getClass().getName()); if (mUnfinishedLoadersCount.decrementAndGet() == 0) { - mSearchAdapter.mergeResults(); + final int resultCount = mSearchAdapter.mergeResults(); + mSearchFeatureProvider.showFeedbackButton(this, getView()); + + if (resultCount == 0) { + mNoResultsView.setVisibility(View.VISIBLE); + } } } @@ -257,6 +278,14 @@ public class SearchFragment extends InstrumentedFragment implements loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */); } + public String getQuery() { + return mQuery; + } + + public List getSearchResults() { + return mSearchAdapter.getSearchResults(); + } + @VisibleForTesting (otherwise = VisibleForTesting.PRIVATE) SearchView makeSearchView(ActionBar actionBar, String query) { final SearchView searchView = new SearchView(actionBar.getThemedContext()); @@ -304,4 +333,4 @@ public class SearchFragment extends InstrumentedFragment implements } } -} +} \ No newline at end of file diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java index 5151b644f49..b76958a60db 100644 --- a/src/com/android/settings/search2/SearchResultsAdapter.java +++ b/src/com/android/settings/search2/SearchResultsAdapter.java @@ -106,8 +106,10 @@ public class SearchResultsAdapter extends Adapter { /** * Merge the results from each of the loaders into one list for the adapter. * Prioritizes results from the local database over installed apps. + * + * @return Number of matched results */ - public void mergeResults() { + public int mergeResults() { final List databaseResults = mResultsMap .get(DatabaseResultLoader.class.getName()); final List installedAppResults = mResultsMap @@ -139,6 +141,8 @@ public class SearchResultsAdapter extends Adapter { mSearchResults.addAll(results); notifyDataSetChanged(); + + return mSearchResults.size(); } public void clearResults() { diff --git a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java similarity index 98% rename from tests/robotests/src/com/android/settings/search/SearchAdapterTest.java rename to tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java index fdcea6f83e8..6100050a5f1 100644 --- a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java @@ -60,7 +60,7 @@ import static org.mockito.Mockito.doReturn; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) -public class SearchAdapterTest { +public class SearchResultsAdapterTest { @Mock private SearchFragment mFragment; @@ -114,7 +114,7 @@ public class SearchAdapterTest { InstalledAppResultLoader.class.getName()); mAdapter.addResultsToMap(getDummyDbResults(), DatabaseResultLoader.class.getName()); - mAdapter.mergeResults(); + int count = mAdapter.mergeResults(); List results = mAdapter.getSearchResults(); assertThat(results.get(0).title).isEqualTo("alpha"); @@ -123,6 +123,7 @@ public class SearchAdapterTest { assertThat(results.get(3).title).isEqualTo("bravo"); assertThat(results.get(4).title).isEqualTo("appCharlie"); assertThat(results.get(5).title).isEqualTo("Charlie"); + assertThat(count).isEqualTo(6); } private List getDummyDbResults() { diff --git a/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java index 72c658ffe91..8b97a918d90 100644 --- a/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java @@ -67,7 +67,6 @@ public class DatabaseResultLoaderTest { private final String summaryOne = "summaryOne"; private final String summaryTwo = "summaryTwo"; private final String summaryThree = "summaryThree"; - private final String summaryFour = "summaryFour"; SQLiteDatabase mDb; diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java index 15b3a674ed7..6a61f528b0c 100644 --- a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java +++ b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java @@ -16,10 +16,13 @@ package com.android.settings.search2; +import android.app.LoaderManager; + import android.content.Context; import android.content.Loader; import android.os.Bundle; +import android.view.View; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsRobolectricTestRunner; @@ -42,6 +45,7 @@ import java.util.List; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -241,4 +245,52 @@ public class SearchFragmentTest { verify(fragment, times(2)).onLoadFinished(any(Loader.class), any(List.class)); } + + @Test + public void whenNoQuery_HideFeedbackIsCalled() { + when(mFeatureFactory.searchFeatureProvider + .getDatabaseSearchLoader(any(Context.class), anyString())) + .thenReturn(new MockDBLoader(RuntimeEnvironment.application)); + when(mFeatureFactory.searchFeatureProvider + .getInstalledAppSearchLoader(any(Context.class), anyString())) + .thenReturn(new MockAppLoader(RuntimeEnvironment.application)); + + ActivityController activityController = + Robolectric.buildActivity(SearchActivity.class); + activityController.setup(); + SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager() + .findFragmentById(R.id.main_content)); + + when(fragment.getLoaderManager()).thenReturn(mock(LoaderManager.class)); + + fragment.onQueryTextChange(""); + + Robolectric.flushForegroundThreadScheduler(); + + verify(mFeatureFactory.searchFeatureProvider).hideFeedbackButton(); + } + + @Test + public void onLoadFinished_ShowsFeedback() { + + when(mFeatureFactory.searchFeatureProvider + .getDatabaseSearchLoader(any(Context.class), anyString())) + .thenReturn(new MockDBLoader(RuntimeEnvironment.application)); + when(mFeatureFactory.searchFeatureProvider + .getInstalledAppSearchLoader(any(Context.class), anyString())) + .thenReturn(new MockAppLoader(RuntimeEnvironment.application)); + + ActivityController activityController = + Robolectric.buildActivity(SearchActivity.class); + activityController.setup(); + SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager() + .findFragmentById(R.id.main_content)); + + fragment.onQueryTextChange("non-empty"); + + Robolectric.flushForegroundThreadScheduler(); + + verify(mFeatureFactory.searchFeatureProvider).showFeedbackButton(any(SearchFragment.class), + any(View.class)); + } }