Merge "Clean up search fragment loader lifecycle."
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2016 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.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:title="@string/search_menu"
|
||||
android:icon="@*android:drawable/ic_search_api_material"
|
||||
android:showAsAction="collapseActionView|ifRoom"
|
||||
android:actionViewClass="android.widget.SearchView"/>
|
||||
</menu>
|
@@ -28,9 +28,9 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.internal.logging.nano.MetricsProto;
|
||||
import com.android.settings.core.InstrumentedFragment;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.core.InstrumentedFragment;
|
||||
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settings.overlay.SupportFeatureProvider;
|
||||
@@ -77,7 +77,7 @@ public final class DashboardContainerFragment extends InstrumentedFragment {
|
||||
|
||||
// check if support tab needs to be selected
|
||||
final String selectedTab = getArguments().
|
||||
getString(EXTRA_SELECT_SETTINGS_TAB, ARG_SUMMARY_TAB);
|
||||
getString(EXTRA_SELECT_SETTINGS_TAB, ARG_SUMMARY_TAB);
|
||||
if (TextUtils.equals(selectedTab, ARG_SUPPORT_TAB)) {
|
||||
mViewPager.setCurrentItem(INDEX_SUPPORT_FRAGMENT);
|
||||
} else {
|
||||
|
@@ -22,7 +22,6 @@ import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@@ -16,7 +16,7 @@
|
||||
package com.android.settings.search2;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.widget.SearchView;
|
||||
import android.content.Context;
|
||||
import android.view.Menu;
|
||||
|
||||
/**
|
||||
@@ -31,8 +31,14 @@ public interface SearchFeatureProvider {
|
||||
|
||||
/**
|
||||
* Inserts the Menu items into Settings activity.
|
||||
*
|
||||
* @param menu Items will be inserted into this menu.
|
||||
* @param activity The activity that precedes SearchActivity.
|
||||
*/
|
||||
void setUpSearchMenu(Menu menu, Activity activity);
|
||||
|
||||
/**
|
||||
* Returns a new loader to search in index database.
|
||||
*/
|
||||
DatabaseResultLoader getDatabaseSearchLoader(Context context, String query);
|
||||
}
|
||||
|
@@ -24,6 +24,9 @@ import android.view.Menu;
|
||||
|
||||
import android.view.MenuItem;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.utils.AsyncLoader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* FeatureProvider for the refactored search code.
|
||||
@@ -60,4 +63,9 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
|
||||
|
||||
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) {
|
||||
return new DatabaseResultLoader(context, query);
|
||||
}
|
||||
}
|
||||
|
@@ -16,107 +16,110 @@
|
||||
|
||||
package com.android.settings.search2;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.app.LoaderManager;
|
||||
import android.content.Context;
|
||||
import android.content.Loader;
|
||||
import android.os.Bundle;
|
||||
import android.app.LoaderManager;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import com.android.internal.logging.nano.MetricsProto;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.InstrumentedFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SearchFragment extends InstrumentedFragment implements
|
||||
SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener,
|
||||
LoaderManager.LoaderCallbacks<List<SearchResult>> {
|
||||
SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<SearchResult>> {
|
||||
|
||||
// State values
|
||||
static final String STATE_QUERY = "query";
|
||||
|
||||
// Loader IDs
|
||||
private static final int DATABASE_LOADER_ID = 0;
|
||||
|
||||
private SearchResultsAdapter mSearchAdapter;
|
||||
@VisibleForTesting
|
||||
String mQuery;
|
||||
|
||||
private SearchFeatureProvider mSearchFeatureProvider;
|
||||
private DatabaseResultLoader mSearchLoader;
|
||||
|
||||
private SearchResultsAdapter mSearchAdapter;
|
||||
private RecyclerView mResultsRecyclerView;
|
||||
private SearchView mSearchView;
|
||||
private MenuItem mSearchMenuItem;
|
||||
|
||||
private String mQuery;
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
mSearchFeatureProvider = FeatureFactory.getFactory(context)
|
||||
.getSearchFeatureProvider(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
mSearchAdapter = new SearchResultsAdapter();
|
||||
|
||||
final LoaderManager loaderManager = getLoaderManager();
|
||||
loaderManager.initLoader(DATABASE_LOADER_ID, null, this);
|
||||
if (savedInstanceState != null) {
|
||||
mQuery = savedInstanceState.getString(STATE_QUERY);
|
||||
getLoaderManager().initLoader(DATABASE_LOADER_ID, null, this);
|
||||
}
|
||||
final ActionBar actionBar = getActivity().getActionBar();
|
||||
actionBar.setCustomView(makeSearchView(actionBar, mQuery));
|
||||
actionBar.setDisplayShowCustomEnabled(true);
|
||||
actionBar.setDisplayShowTitleEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.search_panel_2, container, false);
|
||||
mResultsRecyclerView = (RecyclerView) view.findViewById(R.id.list_results);
|
||||
|
||||
mResultsRecyclerView.setAdapter(mSearchAdapter);
|
||||
mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.search_options_menu, menu);
|
||||
|
||||
|
||||
mSearchMenuItem = menu.findItem(R.id.search);
|
||||
|
||||
mSearchView = (SearchView) mSearchMenuItem.getActionView();
|
||||
mSearchView.setOnQueryTextListener(this);
|
||||
mSearchView.setMaxWidth(Integer.MAX_VALUE);
|
||||
mSearchMenuItem.expandActionView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||
// Return false to prevent the search box from collapsing.
|
||||
return false;
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(STATE_QUERY, mQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String query) {
|
||||
if (query == null || query.equals(mQuery)) {
|
||||
return false;
|
||||
if (TextUtils.equals(query, mQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
mQuery = query;
|
||||
clearLoaders();
|
||||
mSearchAdapter.clearResults();
|
||||
|
||||
final LoaderManager loaderManager = getLoaderManager();
|
||||
loaderManager.restartLoader(DATABASE_LOADER_ID, null, this);
|
||||
if (TextUtils.isEmpty(mQuery)) {
|
||||
getLoaderManager().destroyLoader(DATABASE_LOADER_ID);
|
||||
} else {
|
||||
restartLoaders();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +128,7 @@ public class SearchFragment extends InstrumentedFragment implements
|
||||
|
||||
switch (id) {
|
||||
case DATABASE_LOADER_ID:
|
||||
mSearchLoader = new DatabaseResultLoader(activity, mQuery);
|
||||
mSearchLoader = mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
|
||||
return mSearchLoader;
|
||||
default:
|
||||
return null;
|
||||
@@ -142,17 +145,22 @@ public class SearchFragment extends InstrumentedFragment implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<SearchResult>> loader) { }
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
|
||||
public void onLoaderReset(Loader<List<SearchResult>> loader) {
|
||||
}
|
||||
|
||||
private void clearLoaders() {
|
||||
if (mSearchLoader != null) {
|
||||
mSearchLoader.cancelLoad();
|
||||
mSearchLoader = null;
|
||||
}
|
||||
private void restartLoaders() {
|
||||
final LoaderManager loaderManager = getLoaderManager();
|
||||
loaderManager.restartLoader(DATABASE_LOADER_ID, null /* args */, this /* callback */);
|
||||
}
|
||||
|
||||
private SearchView makeSearchView(ActionBar actionBar, String query) {
|
||||
final SearchView searchView = new SearchView(actionBar.getThemedContext());
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setQuery(query, false /* submitQuery */);
|
||||
searchView.setOnQueryTextListener(this);
|
||||
final LayoutParams lp =
|
||||
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
searchView.setLayoutParams(lp);
|
||||
return searchView;
|
||||
}
|
||||
}
|
||||
|
@@ -28,10 +28,11 @@ import com.android.settings.search2.ResultPayload.PayloadType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
|
||||
private ArrayList<SearchResult> mSearchResults;
|
||||
private HashMap<String, List<SearchResult>> mResultsMap;
|
||||
private final List<SearchResult> mSearchResults;
|
||||
private final Map<String, List<SearchResult>> mResultsMap;
|
||||
|
||||
public SearchResultsAdapter() {
|
||||
mSearchResults = new ArrayList<>();
|
||||
@@ -45,13 +46,19 @@ public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
|
||||
return;
|
||||
}
|
||||
mResultsMap.put(loaderClassName, freshResults);
|
||||
mSearchResults = mergeMappedResults();
|
||||
mSearchResults.addAll(mergeMappedResults());
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void clearResults() {
|
||||
mSearchResults.clear();
|
||||
mResultsMap.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private ArrayList<SearchResult> mergeMappedResults() {
|
||||
ArrayList<SearchResult> mergedResults = new ArrayList<>();
|
||||
for(String key : mResultsMap.keySet()) {
|
||||
for (String key : mResultsMap.keySet()) {
|
||||
mergedResults.addAll(mResultsMap.get(key));
|
||||
}
|
||||
return mergedResults;
|
||||
@@ -60,7 +67,7 @@ public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
|
||||
@Override
|
||||
public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
switch(viewType) {
|
||||
switch (viewType) {
|
||||
case PayloadType.INTENT:
|
||||
View view = inflater.inflate(R.layout.search_intent_item, parent, false);
|
||||
return new IntentSearchViewHolder(view);
|
||||
@@ -95,7 +102,7 @@ public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ArrayList<SearchResult> getSearchResults() {
|
||||
public List<SearchResult> getSearchResults() {
|
||||
return mSearchResults;
|
||||
}
|
||||
}
|
||||
|
@@ -20,20 +20,25 @@ package com.android.settings.search;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.search2.*;
|
||||
import com.android.settings.search2.DatabaseResultLoader;
|
||||
import com.android.settings.search2.IntentPayload;
|
||||
import com.android.settings.search2.ResultPayload;
|
||||
import com.android.settings.search2.SearchResult;
|
||||
import com.android.settings.search2.SearchResult.Builder;
|
||||
import com.android.settings.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import com.android.settings.search2.SearchResultsAdapter;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
@@ -78,7 +83,7 @@ public class SearchAdapterTest {
|
||||
|
||||
@Test
|
||||
public void testNoResultsAdded_EmptyListReturned() {
|
||||
ArrayList<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
List<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
assertThat(updatedResults).isEmpty();
|
||||
}
|
||||
|
||||
@@ -88,7 +93,7 @@ public class SearchAdapterTest {
|
||||
ArrayList<SearchResult> intentResults = getIntentSampleResults();
|
||||
mAdapter.mergeResults(intentResults, mLoaderClassName);
|
||||
|
||||
ArrayList<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
List<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
assertThat(updatedResults).containsAllIn(intentResults);
|
||||
}
|
||||
|
||||
@@ -98,7 +103,7 @@ public class SearchAdapterTest {
|
||||
mAdapter.mergeResults(intentResults, mLoaderClassName);
|
||||
mAdapter.mergeResults(intentResults, mLoaderClassName);
|
||||
|
||||
ArrayList<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
List<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||
assertThat(updatedResults).containsAllIn(intentResults);
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.settings.search2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ActivityController;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(SettingsRobolectricTestRunner.class)
|
||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||
public class SearchFragmentTest {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private DatabaseResultLoader mDatabaseResultLoader;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
FakeFeatureFactory.setupForTest(mContext);
|
||||
mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void screenRotate_shouldPersistQuery() {
|
||||
when(mFeatureFactory.searchFeatureProvider
|
||||
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
||||
.thenReturn(mDatabaseResultLoader);
|
||||
|
||||
final Bundle bundle = new Bundle();
|
||||
final String testQuery = "test";
|
||||
ActivityController<SearchActivity> activityController =
|
||||
Robolectric.buildActivity(SearchActivity.class);
|
||||
activityController.setup();
|
||||
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
|
||||
.findFragmentById(R.id.main_content);
|
||||
|
||||
fragment.mQuery = testQuery;
|
||||
|
||||
activityController.saveInstanceState(bundle).pause().stop().destroy();
|
||||
|
||||
activityController = Robolectric.buildActivity(SearchActivity.class);
|
||||
activityController.setup(bundle);
|
||||
|
||||
verify(mFeatureFactory.searchFeatureProvider)
|
||||
.getDatabaseSearchLoader(any(Context.class), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryTextChange_shouldTriggerLoader() {
|
||||
when(mFeatureFactory.searchFeatureProvider
|
||||
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
||||
.thenReturn(mDatabaseResultLoader);
|
||||
|
||||
final String testQuery = "test";
|
||||
ActivityController<SearchActivity> activityController =
|
||||
Robolectric.buildActivity(SearchActivity.class);
|
||||
activityController.setup();
|
||||
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
|
||||
.findFragmentById(R.id.main_content);
|
||||
|
||||
fragment.onQueryTextChange(testQuery);
|
||||
|
||||
verify(mFeatureFactory.searchFeatureProvider)
|
||||
.getDatabaseSearchLoader(any(Context.class), anyString());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user