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 int LOADER_ID_INDEXABLE_CONTENT_MONITOR = 1;
public static final int LOADER_ID_INDEXABLE_CONTENT_MONITOR = 1;
// Constants for state save/restore
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.search.IndexDatabaseHelper;
import com.android.settings.search.Indexable;
import com.android.settings.search.IndexingCallback;
import com.android.settings.search.SearchIndexableRaw;
import com.android.settings.search.SearchIndexableResources;
@@ -132,7 +133,8 @@ public class DatabaseIndexingManager {
private final String mBaseAuthority;
private final AtomicBoolean mIsAvailable = new AtomicBoolean(false);
@VisibleForTesting
final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
@VisibleForTesting
final UpdateData mDataToProcess = new UpdateData();
@@ -147,17 +149,13 @@ public class DatabaseIndexingManager {
mContext = context;
}
public boolean isAvailable() {
return mIsAvailable.get();
public boolean isIndexingComplete() {
return mIsIndexingComplete.get();
}
public void indexDatabase() {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
performIndexing();
}
});
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
/**
@@ -171,15 +169,12 @@ public class DatabaseIndexingManager {
final List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final String localeStr = Locale.getDefault().toString();
final String fingerprint = Build.FINGERPRINT;
String localeStr = Locale.getDefault().toString();
String fingerprint = Build.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) {
final SQLiteDatabase db = getWritableDatabase();
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
rebuildDatabase();
}
for (final ResolveInfo info : list) {
@@ -217,6 +212,18 @@ public class DatabaseIndexingManager {
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.
* First, the data to be updated and all non-indexable keys are copied locally.
@@ -229,7 +236,6 @@ public class DatabaseIndexingManager {
*/
@VisibleForTesting
void updateDatabase(boolean needsReindexing, String localeStr) {
mIsAvailable.set(false);
final UpdateData copy;
synchronized (mDataToProcess) {
@@ -264,8 +270,6 @@ public class DatabaseIndexingManager {
} finally {
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 com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.IndexingCallback;
/**
* FeatureProvider for Settings Search
@@ -68,7 +69,12 @@ public interface SearchFeatureProvider {
/**
* 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.

View File

@@ -19,6 +19,7 @@ package com.android.settings.search2;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -26,6 +27,9 @@ import android.view.MenuItem;
import com.android.settings.R;
import com.android.settings.applications.PackageManagerWrapperImpl;
import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.IndexingCallback;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* FeatureProvider for the refactored search code.
@@ -88,6 +92,11 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
return mDatabaseIndexingManager;
}
@Override
public boolean isIndexingComplete(Context context) {
return getIndexingManager(context).isIndexingComplete();
}
public SiteMapManager getSiteMapManager() {
if (mSiteMapManager == null) {
mSiteMapManager = new SiteMapManager();
@@ -96,9 +105,9 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
}
@Override
public void updateIndex(Context context) {
public void updateIndex(Context context, IndexingCallback callback) {
long indexStartTime = System.currentTimeMillis();
getIndexingManager(context).indexDatabase();
getIndexingManager(context).indexDatabase(callback);
Log.d(TAG, "IndexDatabase() took " +
(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.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.IndexingCallback;
import java.util.List;
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,
LoaderManager.LoaderCallbacks<List<? extends SearchResult>> {
LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
private static final String TAG = "SearchFragment";
@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";
// Loader IDs
private static final int LOADER_ID_DATABASE = 1;
private static final int LOADER_ID_INSTALLED_APPS = 2;
@VisibleForTesting
static final int LOADER_ID_DATABASE = 1;
@VisibleForTesting
static final int LOADER_ID_INSTALLED_APPS = 2;
private static final int NUM_QUERY_LOADERS = 2;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@VisibleForTesting
AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
// Logging
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@VisibleForTesting
static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
@VisibleForTesting
String mQuery;
private boolean mNeverEnteredQuery = true;
private boolean mShowingSavedQuery;
@VisibleForTesting
boolean mShowingSavedQuery;
private int mResultClickCount;
private MetricsFeatureProvider mMetricsFeatureProvider;
private SavedQueryController mSavedQueryController;
@VisibleForTesting
SavedQueryController mSavedQueryController;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchFeatureProvider mSearchFeatureProvider;
@@ -87,7 +102,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
RecyclerView mResultsRecyclerView;
private SearchView mSearchView;
@VisibleForTesting
SearchView mSearchView;
private LinearLayout mNoResultsView;
@VisibleForTesting
@@ -116,6 +132,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final LoaderManager loaderManager = getLoaderManager();
mSearchAdapter = new SearchResultsAdapter(this);
mSavedQueryController = new SavedQueryController(
@@ -127,15 +144,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
mResultClickCount = savedInstanceState.getInt(STATE_RESULT_CLICK_COUNT);
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 {
mShowingSavedQuery = true;
mSavedQueryController.loadSavedQueries();
}
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
if (!Utils.isLowStorage(activity)) {
mSearchFeatureProvider.updateIndex(activity);
mSearchFeatureProvider.updateIndex(activity, this /* indexingCallback */);
} else {
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
public void onResume() {
super.onResume();
if (TextUtils.isEmpty(mQuery)) {
return;
}
final String query = mQuery;
mQuery = "";
onQueryTextChange(query);
requery();
}
@Override
@@ -218,6 +223,11 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
mNeverEnteredQuery = false;
mQuery = query;
// If indexing is not finished, register the query text, but don't search.
if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
return true;
}
if (isEmptyQuery) {
final LoaderManager loaderManager = getLoaderManager();
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) {
}
/**
* 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() {
mSavedQueryController.saveQuery(mQuery);
mResultClickCount++;
@@ -309,6 +335,15 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
return mSearchAdapter.getSearchResults();
}
private void requery() {
if (TextUtils.isEmpty(mQuery)) {
return;
}
final String query = mQuery;
mQuery = "";
onQueryTextChange(query);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchView makeSearchView(ActionBar actionBar, String query) {
final SearchView searchView = new SearchView(actionBar.getThemedContext());

View File

@@ -18,7 +18,6 @@
package com.android.settings.search;
import android.app.Activity;
import android.content.Context;
import android.view.Menu;
import com.android.settings.SettingsRobolectricTestRunner;
@@ -35,14 +34,11 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
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.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -73,19 +69,10 @@ public class SearchFeatureProviderImplTest {
}
@Test
public void getSiteMapManager_shouldCacheInstanec() {
public void getSiteMapManager_shouldCacheInstance() {
final SiteMapManager manager1 = mProvider.getSiteMapManager();
final SiteMapManager manager2 = mProvider.getSiteMapManager();
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.TestConfig;
import com.android.settings.search.IndexDatabaseHelper;
import com.android.settings.search.IndexingCallback;
import com.android.settings.search.SearchIndexableRaw;
import com.android.settings.testutils.DatabaseTestUtils;
import com.android.settings.testutils.shadow.ShadowDatabaseIndexingUtils;
@@ -47,6 +48,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
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.anyMap;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -933,6 +936,32 @@ public class DatabaseIndexingManagerTest {
assertThat(cursor.getCount()).isEqualTo(1);
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
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.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.search.IndexingCallback;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.Before;
@@ -42,6 +43,7 @@ import org.robolectric.util.ReflectionHelpers;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
@@ -104,10 +106,7 @@ public class SearchFragmentTest {
activityController = Robolectric.buildActivity(SearchActivity.class);
activityController.setup(bundle);
verify(mFeatureFactory.searchFeatureProvider)
.getDatabaseSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider)
.getInstalledAppSearchLoader(any(Context.class), anyString());
assertThat(fragment.mQuery).isEqualTo(testQuery);
}
@Test
@@ -121,6 +120,8 @@ public class SearchFragmentTest {
activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.mQuery = "";
@@ -133,8 +134,6 @@ public class SearchFragmentTest {
.getDatabaseSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, never())
.getInstalledAppSearchLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, times(2))
.getSavedQueryLoader(any(Context.class));
}
@Test
@@ -154,6 +153,8 @@ public class SearchFragmentTest {
activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange(testQuery);
activityController.get().onBackPressed();
@@ -181,15 +182,16 @@ public class SearchFragmentTest {
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = spy((SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController);
fragment.mQuery = "123";
fragment.onQueryTextChange("");
verify(mFeatureFactory.searchFeatureProvider, never())
@@ -215,9 +217,12 @@ public class SearchFragmentTest {
activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content);
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onAttach(null);
verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class));
verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class),
any(IndexingCallback.class));
}
@Test
@@ -237,6 +242,8 @@ public class SearchFragmentTest {
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange("non-empty");
@@ -261,11 +268,11 @@ public class SearchFragmentTest {
activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
when(fragment.getLoaderManager()).thenReturn(mock(LoaderManager.class));
fragment.onQueryTextChange("");
Robolectric.flushForegroundThreadScheduler();
verify(mFeatureFactory.searchFeatureProvider).hideFeedbackButton();
@@ -281,18 +288,65 @@ public class SearchFragmentTest {
.thenReturn(new MockAppLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.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 =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) activityController.get().getFragmentManager()
.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),
any(View.class));
fragment.onIndexingFinished();
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();
}
}