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 extends SearchResult> databaseResults = mResultsMap
.get(DatabaseResultLoader.class.getName());
final List extends SearchResult> 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));
+ }
}