Prevent race conditions after dropping database

Block all loaders from accessing the database
before indexing is complete.

Bug: 37501479
Test: make RunSettingsRoboTests
Change-Id: I2af98dcf6bceacbeffa70bd40879c97bb0cbc828
Merged-In: I02f8423c0ffc27abbb8ceb61a8c47d2f0796d0bb
This commit is contained in:
Matthew Fritze
2017-04-25 09:38:56 -07:00
parent 4f5b13f97a
commit ab0b3f6790
9 changed files with 240 additions and 75 deletions

View File

@@ -79,7 +79,7 @@ public class SettingsActivity extends SettingsDrawerActivity
private static final String LOG_TAG = "Settings"; private static final String LOG_TAG = "Settings";
private static final int LOADER_ID_INDEXABLE_CONTENT_MONITOR = 1; public static final int LOADER_ID_INDEXABLE_CONTENT_MONITOR = 1;
// Constants for state save/restore // Constants for state save/restore
private static final String SAVE_KEY_CATEGORIES = ":settings:categories"; private static final String SAVE_KEY_CATEGORIES = ":settings:categories";

View File

@@ -0,0 +1,12 @@
package com.android.settings.search;
/**
* Callback for Settings search indexing.
*/
public interface IndexingCallback {
/**
* Called when Indexing is finished.
*/
void onIndexingFinished();
}

View File

@@ -42,6 +42,7 @@ import android.util.Xml;
import com.android.settings.core.PreferenceController; import com.android.settings.core.PreferenceController;
import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.search.IndexDatabaseHelper;
import com.android.settings.search.Indexable; import com.android.settings.search.Indexable;
import com.android.settings.search.IndexingCallback;
import com.android.settings.search.SearchIndexableRaw; import com.android.settings.search.SearchIndexableRaw;
import com.android.settings.search.SearchIndexableResources; import com.android.settings.search.SearchIndexableResources;
@@ -132,7 +133,8 @@ public class DatabaseIndexingManager {
private final String mBaseAuthority; private final String mBaseAuthority;
private final AtomicBoolean mIsAvailable = new AtomicBoolean(false); @VisibleForTesting
final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
@VisibleForTesting @VisibleForTesting
final UpdateData mDataToProcess = new UpdateData(); final UpdateData mDataToProcess = new UpdateData();
@@ -147,17 +149,13 @@ public class DatabaseIndexingManager {
mContext = context; mContext = context;
} }
public boolean isAvailable() { public boolean isIndexingComplete() {
return mIsAvailable.get(); return mIsIndexingComplete.get();
} }
public void indexDatabase() { public void indexDatabase(IndexingCallback callback) {
AsyncTask.execute(new Runnable() { IndexingTask task = new IndexingTask(callback);
@Override task.execute();
public void run() {
performIndexing();
}
});
} }
/** /**
@@ -171,15 +169,12 @@ public class DatabaseIndexingManager {
final List<ResolveInfo> list = final List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0); mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final String localeStr = Locale.getDefault().toString(); String localeStr = Locale.getDefault().toString();
final String fingerprint = Build.FINGERPRINT; String fingerprint = Build.FINGERPRINT;
final boolean isFullIndex = isFullIndex(localeStr, fingerprint); final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
// Drop the database when the locale or build has changed. This eliminates rows which are
// dynamically inserted in the old language, or deprecated settings.
if (isFullIndex) { if (isFullIndex) {
final SQLiteDatabase db = getWritableDatabase(); rebuildDatabase();
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
} }
for (final ResolveInfo info : list) { for (final ResolveInfo info : list) {
@@ -217,6 +212,18 @@ public class DatabaseIndexingManager {
return !isLocaleIndexed || !isBuildIndexed; return !isLocaleIndexed || !isBuildIndexed;
} }
/**
* Reconstruct the database in the following cases:
* - Language has changed
* - Build has changed
*/
private void rebuildDatabase() {
// Drop the database when the locale or build has changed. This eliminates rows which are
// dynamically inserted in the old language, or deprecated settings.
final SQLiteDatabase db = getWritableDatabase();
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
}
/** /**
* Adds new data to the database and verifies the correctness of the ENABLED column. * Adds new data to the database and verifies the correctness of the ENABLED column.
* First, the data to be updated and all non-indexable keys are copied locally. * First, the data to be updated and all non-indexable keys are copied locally.
@@ -229,7 +236,6 @@ public class DatabaseIndexingManager {
*/ */
@VisibleForTesting @VisibleForTesting
void updateDatabase(boolean needsReindexing, String localeStr) { void updateDatabase(boolean needsReindexing, String localeStr) {
mIsAvailable.set(false);
final UpdateData copy; final UpdateData copy;
synchronized (mDataToProcess) { synchronized (mDataToProcess) {
@@ -264,8 +270,6 @@ public class DatabaseIndexingManager {
} finally { } finally {
database.endTransaction(); database.endTransaction();
} }
mIsAvailable.set(true);
} }
/** /**
@@ -1223,4 +1227,33 @@ public class DatabaseIndexingManager {
} }
} }
} }
public class IndexingTask extends AsyncTask<Void, Void, Void> {
@VisibleForTesting
IndexingCallback mCallback;
public IndexingTask(IndexingCallback callback) {
mCallback = callback;
}
@Override
protected void onPreExecute() {
mIsIndexingComplete.set(false);
}
@Override
protected Void doInBackground(Void... voids) {
performIndexing();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
mIsIndexingComplete.set(true);
if (mCallback != null) {
mCallback.onIndexingFinished();
}
}
}
} }

View File

@@ -21,6 +21,7 @@ import android.view.Menu;
import android.view.View; import android.view.View;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.IndexingCallback;
/** /**
* FeatureProvider for Settings Search * FeatureProvider for Settings Search
@@ -68,7 +69,12 @@ public interface SearchFeatureProvider {
/** /**
* Updates the Settings indexes * Updates the Settings indexes
*/ */
void updateIndex(Context context); void updateIndex(Context context, IndexingCallback callback);
/**
* @returns true when indexing is complete.
*/
boolean isIndexingComplete(Context context);
/** /**
* Initializes the feedback button in case it was dismissed. * Initializes the feedback button in case it was dismissed.

View File

@@ -19,6 +19,7 @@ package com.android.settings.search2;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.support.annotation.VisibleForTesting;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -26,6 +27,9 @@ import android.view.MenuItem;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.applications.PackageManagerWrapperImpl; import com.android.settings.applications.PackageManagerWrapperImpl;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.IndexingCallback;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* FeatureProvider for the refactored search code. * FeatureProvider for the refactored search code.
@@ -88,6 +92,11 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
return mDatabaseIndexingManager; return mDatabaseIndexingManager;
} }
@Override
public boolean isIndexingComplete(Context context) {
return getIndexingManager(context).isIndexingComplete();
}
public SiteMapManager getSiteMapManager() { public SiteMapManager getSiteMapManager() {
if (mSiteMapManager == null) { if (mSiteMapManager == null) {
mSiteMapManager = new SiteMapManager(); mSiteMapManager = new SiteMapManager();
@@ -96,9 +105,9 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
} }
@Override @Override
public void updateIndex(Context context) { public void updateIndex(Context context, IndexingCallback callback) {
long indexStartTime = System.currentTimeMillis(); long indexStartTime = System.currentTimeMillis();
getIndexingManager(context).indexDatabase(); getIndexingManager(context).indexDatabase(callback);
Log.d(TAG, "IndexDatabase() took " + Log.d(TAG, "IndexDatabase() took " +
(System.currentTimeMillis() - indexStartTime) + " ms"); (System.currentTimeMillis() - indexStartTime) + " ms");
} }

View File

@@ -41,12 +41,23 @@ import com.android.settings.Utils;
import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.InstrumentedFragment;
import com.android.settings.core.instrumentation.MetricsFeatureProvider; import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.IndexingCallback;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/**
* This fragment manages the lifecycle of indexing and searching.
*
* In onCreate, the indexing process is initiated in DatabaseIndexingManager.
* While the indexing is happening, loaders are blocked from accessing the database, but the user
* is free to start typing their query.
*
* When the indexing is complete, the fragment gets a callback to initialize the loaders and search
* the query if the user has entered text.
*/
public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener, public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<List<? extends SearchResult>> { LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
private static final String TAG = "SearchFragment"; private static final String TAG = "SearchFragment";
@VisibleForTesting @VisibleForTesting
@@ -59,26 +70,30 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count"; private static final String STATE_RESULT_CLICK_COUNT = "state_result_click_count";
// Loader IDs // Loader IDs
private static final int LOADER_ID_DATABASE = 1; @VisibleForTesting
private static final int LOADER_ID_INSTALLED_APPS = 2; static final int LOADER_ID_DATABASE = 1;
@VisibleForTesting
static final int LOADER_ID_INSTALLED_APPS = 2;
private static final int NUM_QUERY_LOADERS = 2; private static final int NUM_QUERY_LOADERS = 2;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting
AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS); AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
// Logging // Logging
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting
static final String RESULT_CLICK_COUNT = "settings_search_result_click_count"; static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
@VisibleForTesting @VisibleForTesting
String mQuery; String mQuery;
private boolean mNeverEnteredQuery = true; private boolean mNeverEnteredQuery = true;
private boolean mShowingSavedQuery; @VisibleForTesting
boolean mShowingSavedQuery;
private int mResultClickCount; private int mResultClickCount;
private MetricsFeatureProvider mMetricsFeatureProvider; private MetricsFeatureProvider mMetricsFeatureProvider;
private SavedQueryController mSavedQueryController; @VisibleForTesting
SavedQueryController mSavedQueryController;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchFeatureProvider mSearchFeatureProvider; SearchFeatureProvider mSearchFeatureProvider;
@@ -87,7 +102,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
RecyclerView mResultsRecyclerView; RecyclerView mResultsRecyclerView;
private SearchView mSearchView; @VisibleForTesting
SearchView mSearchView;
private LinearLayout mNoResultsView; private LinearLayout mNoResultsView;
@VisibleForTesting @VisibleForTesting
@@ -116,6 +132,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(true); setHasOptionsMenu(true);
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
mSearchAdapter = new SearchResultsAdapter(this); mSearchAdapter = new SearchResultsAdapter(this);
mSavedQueryController = new SavedQueryController( mSavedQueryController = new SavedQueryController(
@@ -127,15 +144,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY); mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT); mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY); mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
if (mShowingSavedQuery) {
mSavedQueryController.loadSavedQueries();
} else {
loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
}
} else { } else {
mShowingSavedQuery = true; mShowingSavedQuery = true;
mSavedQueryController.loadSavedQueries();
} }
final Activity activity = getActivity(); final Activity activity = getActivity();
@@ -148,7 +158,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
// Run the Index update only if we have some space // Run the Index update only if we have some space
if (!Utils.isLowStorage(activity)) { if (!Utils.isLowStorage(activity)) {
mSearchFeatureProvider.updateIndex(activity); mSearchFeatureProvider.updateIndex(activity, this /* indexingCallback */);
} else { } else {
Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!"); Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
} }
@@ -170,12 +180,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (TextUtils.isEmpty(mQuery)) { requery();
return;
}
final String query = mQuery;
mQuery = "";
onQueryTextChange(query);
} }
@Override @Override
@@ -218,6 +223,11 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
mNeverEnteredQuery = false; mNeverEnteredQuery = false;
mQuery = query; mQuery = query;
// If indexing is not finished, register the query text, but don't search.
if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
return true;
}
if (isEmptyQuery) { if (isEmptyQuery) {
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(LOADER_ID_DATABASE); loaderManager.destroyLoader(LOADER_ID_DATABASE);
@@ -276,6 +286,22 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
public void onLoaderReset(Loader<List<? extends SearchResult>> loader) { public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
} }
/**
* Gets called when Indexing is completed.
*/
@Override
public void onIndexingFinished() {
if (mShowingSavedQuery) {
mSavedQueryController.loadSavedQueries();
} else {
final LoaderManager loaderManager = getLoaderManager();
loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
}
requery();
}
public void onSearchResultClicked() { public void onSearchResultClicked() {
mSavedQueryController.saveQuery(mQuery); mSavedQueryController.saveQuery(mQuery);
mResultClickCount++; mResultClickCount++;
@@ -309,6 +335,15 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
return mSearchAdapter.getSearchResults(); return mSearchAdapter.getSearchResults();
} }
private void requery() {
if (TextUtils.isEmpty(mQuery)) {
return;
}
final String query = mQuery;
mQuery = "";
onQueryTextChange(query);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchView makeSearchView(ActionBar actionBar, String query) { SearchView makeSearchView(ActionBar actionBar, String query) {
final SearchView searchView = new SearchView(actionBar.getThemedContext()); final SearchView searchView = new SearchView(actionBar.getThemedContext());

View File

@@ -18,7 +18,6 @@
package com.android.settings.search; package com.android.settings.search;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.view.Menu; import android.view.Menu;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
@@ -35,14 +34,11 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -73,19 +69,10 @@ public class SearchFeatureProviderImplTest {
} }
@Test @Test
public void getSiteMapManager_shouldCacheInstanec() { public void getSiteMapManager_shouldCacheInstance() {
final SiteMapManager manager1 = mProvider.getSiteMapManager(); final SiteMapManager manager1 = mProvider.getSiteMapManager();
final SiteMapManager manager2 = mProvider.getSiteMapManager(); final SiteMapManager manager2 = mProvider.getSiteMapManager();
assertThat(manager1).isSameAs(manager2); assertThat(manager1).isSameAs(manager2);
} }
@Test
public void testUpdateIndexNewSearch_UsesDatabaseIndexingManager() {
mProvider = spy(new SearchFeatureProviderImpl());
when(mProvider.isEnabled(mActivity)).thenReturn(true);
mProvider.updateIndex(mActivity);
verify(mProvider).getIndexingManager(any(Context.class));
}
} }

View File

@@ -37,6 +37,7 @@ import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.search.IndexDatabaseHelper;
import com.android.settings.search.IndexingCallback;
import com.android.settings.search.SearchIndexableRaw; import com.android.settings.search.SearchIndexableRaw;
import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.DatabaseTestUtils;
import com.android.settings.testutils.shadow.ShadowDatabaseIndexingUtils; import com.android.settings.testutils.shadow.ShadowDatabaseIndexingUtils;
@@ -47,6 +48,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowContentResolver; import org.robolectric.shadows.ShadowContentResolver;
@@ -67,6 +69,7 @@ import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyList;
import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyMap;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
@@ -933,6 +936,32 @@ public class DatabaseIndexingManagerTest {
assertThat(cursor.getCount()).isEqualTo(1); assertThat(cursor.getCount()).isEqualTo(1);
assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE); assertThat(cursor.getString(2)).isEqualTo(TITLE_ONE);
} }
@Test
public void testUpdateAsyncTask_onPostExecute_performsCallback() {
IndexingCallback callback = mock(IndexingCallback.class);
DatabaseIndexingManager.IndexingTask task = mManager.new IndexingTask(callback);
task.execute();
Robolectric.flushForegroundThreadScheduler();
verify(callback).onIndexingFinished();
}
@Test
public void testUpdateAsyncTask_onPostExecute_setsIndexingComplete() {
SearchFeatureProviderImpl provider = new SearchFeatureProviderImpl();
DatabaseIndexingManager manager = spy(provider.getIndexingManager(mContext));
DatabaseIndexingManager.IndexingTask task = manager.new IndexingTask(null);
doNothing().when(manager).performIndexing();
task.execute();
Robolectric.flushForegroundThreadScheduler();
assertThat(provider.isIndexingComplete(mContext)).isTrue();
}
// Util functions // Util functions
private SearchIndexableRaw getFakeRaw() { private SearchIndexableRaw getFakeRaw() {

View File

@@ -26,6 +26,7 @@ import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.search.IndexingCallback;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.Before; import org.junit.Before;
@@ -42,6 +43,7 @@ import org.robolectric.util.ReflectionHelpers;
import java.util.List; import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
@@ -104,10 +106,7 @@ public class SearchFragmentTest {
activityController = Robolectric.buildActivity(SearchActivity.class); activityController = Robolectric.buildActivity(SearchActivity.class);
activityController.setup(bundle); activityController.setup(bundle);
verify(mFeatureFactory.searchFeatureProvider) assertThat(fragment.mQuery).isEqualTo(testQuery);
.getDatabaseSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider)
.getInstalledAppSearchLoader(any(Context.class), anyString());
} }
@Test @Test
@@ -121,6 +120,8 @@ public class SearchFragmentTest {
activityController.setup(); activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content); .findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.mQuery = ""; fragment.mQuery = "";
@@ -133,8 +134,6 @@ public class SearchFragmentTest {
.getDatabaseSearchLoader(any(Context.class), anyString()); .getDatabaseSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
.getInstalledAppSearchLoader(any(Context.class), anyString()); .getInstalledAppSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, times(2))
.getSavedQueryLoader(any(Context.class));
} }
@Test @Test
@@ -154,6 +153,8 @@ public class SearchFragmentTest {
activityController.setup(); activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content); .findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange(testQuery); fragment.onQueryTextChange(testQuery);
activityController.get().onBackPressed(); activityController.get().onBackPressed();
@@ -181,15 +182,16 @@ public class SearchFragmentTest {
.thenReturn(mInstalledAppResultLoader); .thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController = ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class); Robolectric.buildActivity(SearchActivity.class);
activityController.setup(); activityController.setup();
SearchFragment fragment = spy((SearchFragment) activityController.get().getFragmentManager() SearchFragment fragment = spy((SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content)); .findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController); ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController);
fragment.mQuery = "123"; fragment.mQuery = "123";
fragment.onQueryTextChange(""); fragment.onQueryTextChange("");
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
@@ -215,9 +217,12 @@ public class SearchFragmentTest {
activityController.setup(); activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content); .findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onAttach(null); fragment.onAttach(null);
verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class)); verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class),
any(IndexingCallback.class));
} }
@Test @Test
@@ -237,6 +242,8 @@ public class SearchFragmentTest {
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content)); .findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange("non-empty"); fragment.onQueryTextChange("non-empty");
@@ -261,11 +268,11 @@ public class SearchFragmentTest {
activityController.setup(); activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content)); .findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
when(fragment.getLoaderManager()).thenReturn(mock(LoaderManager.class)); when(fragment.getLoaderManager()).thenReturn(mock(LoaderManager.class));
fragment.onQueryTextChange(""); fragment.onQueryTextChange("");
Robolectric.flushForegroundThreadScheduler(); Robolectric.flushForegroundThreadScheduler();
verify(mFeatureFactory.searchFeatureProvider).hideFeedbackButton(); verify(mFeatureFactory.searchFeatureProvider).hideFeedbackButton();
@@ -281,18 +288,65 @@ public class SearchFragmentTest {
.thenReturn(new MockAppLoader(RuntimeEnvironment.application)); .thenReturn(new MockAppLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange("non-empty");
Robolectric.flushForegroundThreadScheduler();
verify(mFeatureFactory.searchFeatureProvider).showFeedbackButton(any(SearchFragment.class),
any(View.class));
}
@Test
public void preIndexingFinished_isIndexingFinishedFlag_isFalse() {
ActivityController<SearchActivity> activityController = ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class); Robolectric.buildActivity(SearchActivity.class);
activityController.setup(); activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager() SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content); .findFragmentById(R.id.main_content);
fragment.onQueryTextChange("non-empty"); when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(false);
}
Robolectric.flushForegroundThreadScheduler(); @Test
public void onIndexingFinished_notShowingSavedQuery_initLoaders() {
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
final LoaderManager loaderManager = mock(LoaderManager.class);
when(fragment.getLoaderManager()).thenReturn(loaderManager);
fragment.mShowingSavedQuery = false;
fragment.mQuery = null;
verify(mFeatureFactory.searchFeatureProvider).showFeedbackButton(any(SearchFragment.class), fragment.onIndexingFinished();
any(View.class));
verify(loaderManager).initLoader(eq(SearchFragment.LOADER_ID_DATABASE),
eq(null), any(LoaderManager.LoaderCallbacks.class));
verify(loaderManager).initLoader(eq(SearchFragment.LOADER_ID_INSTALLED_APPS),
eq(null), any(LoaderManager.LoaderCallbacks.class));
}
@Test
public void onIndexingFinished_showingSavedQuery_loadsSavedQueries() {
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
fragment.mShowingSavedQuery = true;
ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController);
fragment.onIndexingFinished();
verify(fragment.mSavedQueryController).loadSavedQueries();
} }
} }