From 13c43f1900aa625ede5cb470923f0a187666af7e Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Tue, 12 Dec 2017 16:03:22 -0800 Subject: [PATCH] Index Data to build Slices in Settings The indexing is done by taking the indexable fragments from search, grabbing their XML via SearchIndexableResources, and then looking for controllers defined in preferences. For each controller found, we take the combination of the fragment providing the XML and the Preference info to create an indexable row. Buiding a Slice will be handled in a subsquent CL, but a prototype can be found here: ag/3324435 Test: robotests Bug: 67996923 Change-Id: I48668618079bcc3da55ab77b7323ee8e467073af --- res/xml/display_settings.xml | 6 +- .../settings/overlay/FeatureFactory.java | 3 + .../settings/overlay/FeatureFactoryImpl.java | 11 + .../android/settings/slices/SliceData.java | 7 +- .../settings/slices/SliceDataConverter.java | 202 ++++++++++++++++++ .../settings/slices/SlicesDatabaseHelper.java | 91 +++++++- .../slices/SlicesFeatureProvider.java | 17 ++ .../slices/SlicesFeatureProviderImpl.java | 37 ++++ .../settings/slices/SlicesIndexer.java | 102 +++++++++ .../res/xml-mcc999/location_settings.xml | 29 +++ .../settings/search/FakeIndexProvider.java | 11 +- .../slices/FakePreferenceController.java | 33 +++ .../slices/SliceDataConverterTest.java | 89 ++++++++ .../settings/slices/SliceDataTest.java | 37 ++-- .../slices/SlicesDatabaseHelperTest.java | 60 +++++- .../settings/slices/SlicesIndexerTest.java | 152 +++++++++++++ .../settings/testutils/DatabaseTestUtils.java | 21 ++ .../testutils/FakeFeatureFactory.java | 8 + 18 files changed, 882 insertions(+), 34 deletions(-) create mode 100644 src/com/android/settings/slices/SliceDataConverter.java create mode 100644 src/com/android/settings/slices/SlicesFeatureProvider.java create mode 100644 src/com/android/settings/slices/SlicesFeatureProviderImpl.java create mode 100644 src/com/android/settings/slices/SlicesIndexer.java create mode 100644 tests/robotests/res/xml-mcc999/location_settings.xml create mode 100644 tests/robotests/src/com/android/settings/slices/FakePreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java create mode 100644 tests/robotests/src/com/android/settings/slices/SlicesIndexerTest.java diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index 333fd2af705..2d9fd540995 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -25,8 +25,7 @@ + settings:keywords="@string/keywords_display_brightness_level"> @@ -43,7 +42,8 @@ android:key="auto_brightness" android:title="@string/auto_brightness_title" settings:keywords="@string/keywords_display_auto_brightness" - android:summary="@string/auto_brightness_summary" /> + android:summary="@string/auto_brightness_summary" + settings:controller="com.android.settings.display.AutoBrightnessPreferenceController" /> mSliceData; + + public SliceDataConverter(Context context) { + mContext = context; + mSliceData = new ArrayList<>(); + } + + /** + * @return a list of {@link SliceData} to be indexed and later referenced as a Slice. + * + * The collection works as follows: + * - Collects a list of Fragments from {@link SearchIndexableResources}. + * - From each fragment, grab a {@link SearchIndexProvider}. + * - For each provider, collect XML resource layout and a list of + * {@link com.android.settings.core.BasePreferenceController}. + */ + public List getSliceData() { + if (!mSliceData.isEmpty()) { + return mSliceData; + } + + final Collection indexableClasses = SearchIndexableResources.providerValues(); + + for (Class clazz : indexableClasses) { + final String fragmentName = clazz.getName(); + + final SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider( + clazz); + + // CodeInspection test guards against the null check. Keep check in case of bad actors. + if (provider == null) { + Log.e(TAG, fragmentName + " dose not implement Search Index Provider"); + continue; + } + + final List providerSliceData = getSliceDataFromProvider(provider, + fragmentName); + mSliceData.addAll(providerSliceData); + } + + return mSliceData; + } + + private List getSliceDataFromProvider(SearchIndexProvider provider, + String fragmentName) { + final List sliceData = new ArrayList<>(); + + final List resList = + provider.getXmlResourcesToIndex(mContext, true /* enabled */); + + if (resList == null) { + return sliceData; + } + + // TODO (b/67996923) get a list of permanent NIKs and skip the invalid keys. + + for (SearchIndexableResource resource : resList) { + int xmlResId = resource.xmlResId; + if (xmlResId == 0) { + Log.e(TAG, fragmentName + " provides invalid XML (0) in search provider."); + continue; + } + + List xmlSliceData = getSliceDataFromXML(xmlResId, fragmentName); + sliceData.addAll(xmlSliceData); + } + + return sliceData; + } + + private List getSliceDataFromXML(int xmlResId, String fragmentName) { + XmlResourceParser parser = null; + + final List xmlSliceData = new ArrayList<>(); + String key; + String title; + String summary; + @DrawableRes int iconResId; + String controllerClassName; + + try { + parser = mContext.getResources().getXml(xmlResId); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Parse next until start tag is found + } + + String nodeName = parser.getName(); + if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { + throw new RuntimeException( + "XML document must start with tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + final int outerDepth = parser.getDepth(); + final AttributeSet attrs = Xml.asAttributeSet(parser); + final String screenTitle = XmlParserUtils.getDataTitle(mContext, attrs); + + // TODO (b/67996923) Investigate if we need headers for Slices, since they never + // correspond to an actual setting. + SliceData xmlSlice; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + + // TODO (b/67996923) Non-controller Slices should become intent-only slices. + // Note that without a controller, dynamic summaries are impossible. + // TODO (b/67996923) This will not work if preferences have nested intens: + // + // + controllerClassName = XmlParserUtils.getController(mContext, attrs); + if (TextUtils.isEmpty(controllerClassName)) { + continue; + } + + title = XmlParserUtils.getDataTitle(mContext, attrs); + key = XmlParserUtils.getDataKey(mContext, attrs); + iconResId = XmlParserUtils.getDataIcon(mContext, attrs); + summary = XmlParserUtils.getDataSummary(mContext, attrs); + + xmlSlice = new SliceData.Builder() + .setKey(key) + .setTitle(title) + .setSummary(summary) + .setIcon(iconResId) + .setScreenTitle(screenTitle) + .setPreferenceControllerClassName(controllerClassName) + .setFragmentName(fragmentName) + .build(); + + xmlSliceData.add(xmlSlice); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "XML Error parsing PreferenceScreen: ", e); + } catch (IOException e) { + Log.w(TAG, "IO Error parsing PreferenceScreen: ", e); + } catch (Resources.NotFoundException e) { + Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: ", e); + } finally { + if (parser != null) parser.close(); + } + return xmlSliceData; + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseHelper.java b/src/com/android/settings/slices/SlicesDatabaseHelper.java index a74fc812d61..18f8cc91107 100644 --- a/src/com/android/settings/slices/SlicesDatabaseHelper.java +++ b/src/com/android/settings/slices/SlicesDatabaseHelper.java @@ -1,12 +1,30 @@ +/* + * Copyright (C) 2017 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.slices; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; +import android.support.annotation.VisibleForTesting; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; +import java.util.Locale; /** * Defines the schema for the Slices database. @@ -38,7 +56,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { /** * Summary / Subtitle for the setting. */ - String SUBTITLE = "subtitle"; + String SUMMARY = "summary"; /** * Title of the Setting screen on which the Setting lives. @@ -69,7 +87,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { ", " + IndexColumns.TITLE + ", " + - IndexColumns.SUBTITLE + + IndexColumns.SUMMARY + ", " + IndexColumns.SCREENTITLE + ", " + @@ -82,7 +100,16 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { private final Context mContext; - public SlicesDatabaseHelper(Context context) { + private static SlicesDatabaseHelper sSingleton; + + public static synchronized SlicesDatabaseHelper getInstance(Context context) { + if (sSingleton == null) { + sSingleton = new SlicesDatabaseHelper(context); + } + return sSingleton; + } + + private SlicesDatabaseHelper(Context context) { super(context, DATABASE_NAME, null /* CursorFactor */, DATABASE_VERSION); mContext = context; } @@ -100,7 +127,11 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { } } - @VisibleForTesting + /** + * Drops the currently stored databases rebuilds them. + * Also un-marks the state of the data such that any subsequent call to + * {@link#isNewIndexingState(Context)} will return {@code true}. + */ void reconstruct(SQLiteDatabase db) { mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() @@ -110,13 +141,61 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { createDatabases(db); } + /** + * Marks the current state of the device for the validity of the data. Should be called after + * a full index of the TABLE_SLICES_INDEX. + */ + public void setIndexedState() { + setBuildIndexed(); + setLocaleIndexed(); + } + + /** + * Indicates if the indexed slice data reflects the current state of the phone. + * + * @return {@code true} if database should be rebuilt, {@code false} otherwise. + */ + public boolean isSliceDataIndexed() { + return isBuildIndexed() && isLocaleIndexed(); + } + private void createDatabases(SQLiteDatabase db) { db.execSQL(CREATE_SLICES_TABLE); Log.d(TAG, "Created databases"); } - private void dropTables(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SLICES_INDEX); } + + private void setBuildIndexed() { + mContext.getSharedPreferences(SHARED_PREFS_TAG, 0 /* mode */) + .edit() + .putBoolean(getBuildTag(), true /* value */) + .apply(); + } + + private void setLocaleIndexed() { + mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) + .edit() + .putBoolean(Locale.getDefault().toString(), true /* value */) + .apply(); + } + + private boolean isBuildIndexed() { + return mContext.getSharedPreferences(SHARED_PREFS_TAG, + Context.MODE_PRIVATE) + .getBoolean(getBuildTag(), false /* default */); + } + + private boolean isLocaleIndexed() { + return mContext.getSharedPreferences(SHARED_PREFS_TAG, + Context.MODE_PRIVATE) + .getBoolean(Locale.getDefault().toString(), false /* default */); + } + + @VisibleForTesting + String getBuildTag() { + return Build.FINGERPRINT; + } } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesFeatureProvider.java b/src/com/android/settings/slices/SlicesFeatureProvider.java new file mode 100644 index 00000000000..cbf1b75ed1b --- /dev/null +++ b/src/com/android/settings/slices/SlicesFeatureProvider.java @@ -0,0 +1,17 @@ +package com.android.settings.slices; + +import android.content.Context; + +/** + * Manages Slices in Settings. + */ +public interface SlicesFeatureProvider { + + boolean DEBUG = false; + + SlicesIndexer getSliceIndexer(Context context); + + SliceDataConverter getSliceDataConverter(Context context); + + void indexSliceData(Context context); +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java new file mode 100644 index 00000000000..34ef884f641 --- /dev/null +++ b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java @@ -0,0 +1,37 @@ +package com.android.settings.slices; + +import android.content.Context; + +import com.android.settingslib.utils.ThreadUtils; + +/** + * Manages Slices in Settings. + */ +public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { + + private SlicesIndexer mSlicesIndexer; + private SliceDataConverter mSliceDataConverter; + + @Override + public SlicesIndexer getSliceIndexer(Context context) { + if (mSlicesIndexer == null) { + mSlicesIndexer = new SlicesIndexer(context.getApplicationContext()); + } + return mSlicesIndexer; + } + + @Override + public SliceDataConverter getSliceDataConverter(Context context) { + if(mSliceDataConverter == null) { + mSliceDataConverter = new SliceDataConverter(context.getApplicationContext()); + } + return mSliceDataConverter; + } + + @Override + public void indexSliceData(Context context) { + // TODO (b/67996923) add indexing time log + SlicesIndexer indexer = getSliceIndexer(context); + ThreadUtils.postOnBackgroundThread(indexer); + } +} diff --git a/src/com/android/settings/slices/SlicesIndexer.java b/src/com/android/settings/slices/SlicesIndexer.java new file mode 100644 index 00000000000..0297f3fa557 --- /dev/null +++ b/src/com/android/settings/slices/SlicesIndexer.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 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.slices; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.VisibleForTesting; + +import com.android.settings.dashboard.DashboardFragment; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; +import com.android.settings.slices.SlicesDatabaseHelper.Tables; + +import java.util.List; + +/** + * Manages the conversion of {@link DashboardFragment} and {@link BasePreferenceController} to + * indexable data {@link SliceData} to be stored for Slices. + */ +class SlicesIndexer implements Runnable { + + private static final String TAG = "SlicesIndexingManager"; + + private Context mContext; + + private SlicesDatabaseHelper mHelper; + + public SlicesIndexer(Context context) { + mContext = context; + mHelper = SlicesDatabaseHelper.getInstance(mContext); + } + + /** + * Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a + * SQLite database. + */ + @Override + public void run() { + if (mHelper.isSliceDataIndexed()) { + return; + } + + SQLiteDatabase database = mHelper.getWritableDatabase(); + + try { + database.beginTransaction(); + + mHelper.reconstruct(mHelper.getWritableDatabase()); + List indexData = getSliceData(); + insertSliceData(database, indexData); + + mHelper.setIndexedState(); + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + + @VisibleForTesting + List getSliceData() { + return FeatureFactory.getFactory(mContext) + .getSlicesFeatureProvider() + .getSliceDataConverter(mContext) + .getSliceData(); + } + + @VisibleForTesting + void insertSliceData(SQLiteDatabase database, List indexData) { + ContentValues values; + + for (SliceData dataRow : indexData) { + values = new ContentValues(); + values.put(IndexColumns.KEY, dataRow.getKey()); + values.put(IndexColumns.TITLE, dataRow.getTitle()); + values.put(IndexColumns.SUMMARY, dataRow.getSummary()); + values.put(IndexColumns.SCREENTITLE, dataRow.getScreenTitle()); + values.put(IndexColumns.ICON_RESOURCE, dataRow.getIconResource()); + values.put(IndexColumns.FRAGMENT, dataRow.getFragmentClassName()); + values.put(IndexColumns.CONTROLLER, dataRow.getPreferenceController()); + + database.replaceOrThrow(Tables.TABLE_SLICES_INDEX, null /* nullColumnHack */, + values); + } + } +} \ No newline at end of file diff --git a/tests/robotests/res/xml-mcc999/location_settings.xml b/tests/robotests/res/xml-mcc999/location_settings.xml new file mode 100644 index 00000000000..de77bfae006 --- /dev/null +++ b/tests/robotests/res/xml-mcc999/location_settings.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/search/FakeIndexProvider.java b/tests/robotests/src/com/android/settings/search/FakeIndexProvider.java index b2a155319a9..466f5a94f4b 100644 --- a/tests/robotests/src/com/android/settings/search/FakeIndexProvider.java +++ b/tests/robotests/src/com/android/settings/search/FakeIndexProvider.java @@ -20,8 +20,10 @@ package com.android.settings.search; import android.content.Context; import android.provider.SearchIndexableResource; +import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; +import java.util.ArrayList; import java.util.List; public class FakeIndexProvider implements Indexable { @@ -33,7 +35,11 @@ public class FakeIndexProvider implements Indexable { @Override public List getXmlResourcesToIndex(Context context, boolean enabled) { - return null; + List resources = new ArrayList<>(); + SearchIndexableResource res = new SearchIndexableResource(context); + res.xmlResId = R.xml.location_settings; + resources.add(res); + return resources; } @Override @@ -44,7 +50,8 @@ public class FakeIndexProvider implements Indexable { } @Override - public List getPreferenceControllers(Context context) { + public List getPreferenceControllers( + Context context) { return null; } }; diff --git a/tests/robotests/src/com/android/settings/slices/FakePreferenceController.java b/tests/robotests/src/com/android/settings/slices/FakePreferenceController.java new file mode 100644 index 00000000000..f62380f403e --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/FakePreferenceController.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 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.slices; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class FakePreferenceController extends BasePreferenceController { + + public FakePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java b/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java new file mode 100644 index 00000000000..b5c0b5f831f --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 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.slices; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import com.android.settings.TestConfig; +import com.android.settings.search.FakeIndexProvider; +import com.android.settings.search.SearchIndexableResources; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SliceDataConverterTest { + + private final String fakeKey = "key"; + private final String fakeTitle = "title"; + private final String fakeSummary = "summary"; + private final String fakeScreenTitle = "screen_title"; + private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); + private final String fakeControllerName = FakePreferenceController.class.getName(); + + Context mContext; + + private Set mProviderClassesCopy; + + SliceDataConverter mSliceDataConverter; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mProviderClassesCopy = new HashSet<>(SearchIndexableResources.providerValues()); + mSliceDataConverter = new SliceDataConverter(mContext); + } + + @After + public void cleanUp() { + SearchIndexableResources.providerValues().clear(); + SearchIndexableResources.providerValues().addAll(mProviderClassesCopy); + } + + @Test + @Config(qualifiers = "mcc999") + public void testFakeProvider_convertsFakeData() { + SearchIndexableResources.providerValues().clear(); + SearchIndexableResources.providerValues().add(FakeIndexProvider.class); + + List sliceDataList = mSliceDataConverter.getSliceData(); + + assertThat(sliceDataList).hasSize(1); + SliceData fakeSlice = sliceDataList.get(0); + + assertThat(fakeSlice.getKey()).isEqualTo(fakeKey); + assertThat(fakeSlice.getTitle()).isEqualTo(fakeTitle); + assertThat(fakeSlice.getSummary()).isEqualTo(fakeSummary); + assertThat(fakeSlice.getScreenTitle()).isEqualTo(fakeScreenTitle); + assertThat(fakeSlice.getIconResource()).isNotNull(); + assertThat(fakeSlice.getUri()).isNull(); + assertThat(fakeSlice.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(fakeSlice.getPreferenceController()).isEqualTo(fakeControllerName); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SliceDataTest.java b/tests/robotests/src/com/android/settings/slices/SliceDataTest.java index 0e4accabe5e..82183e4a88f 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceDataTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceDataTest.java @@ -104,19 +104,6 @@ public class SliceDataTest { .build(); } - @Test(expected = IllegalStateException.class) - public void testBuilder_noUri_throwsIllegalStateException() { - new SliceData.Builder() - .setKey(KEY) - .setTitle(TITLE) - .setSummary(SUMMARY) - .setScreenTitle(SCREEN_TITLE) - .setIcon(ICON) - .setFragmentName(FRAGMENT_NAME) - .setPreferenceControllerClassName(PREF_CONTROLLER) - .build(); - } - @Test(expected = IllegalStateException.class) public void testBuilder_noPrefController_throwsIllegalStateException() { new SliceData.Builder() @@ -199,6 +186,30 @@ public class SliceDataTest { assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER); } + @Test + public void testBuilder_noUri_buildsMatchingObject() { + SliceData.Builder builder = new SliceData.Builder() + .setKey(KEY) + .setTitle(TITLE) + .setSummary(SUMMARY) + .setScreenTitle(SCREEN_TITLE) + .setIcon(ICON) + .setFragmentName(FRAGMENT_NAME) + .setUri(null) + .setPreferenceControllerClassName(PREF_CONTROLLER); + + SliceData data = builder.build(); + + assertThat(data.getKey()).isEqualTo(KEY); + assertThat(data.getTitle()).isEqualTo(TITLE); + assertThat(data.getSummary()).isEqualTo(SUMMARY); + assertThat(data.getScreenTitle()).isEqualTo(SCREEN_TITLE); + assertThat(data.getIconResource()).isEqualTo(ICON); + assertThat(data.getFragmentClassName()).isEqualTo(FRAGMENT_NAME); + assertThat(data.getUri()).isNull(); + assertThat(data.getPreferenceController()).isEqualTo(PREF_CONTROLLER); + } + @Test public void testEquality_identicalObjects() { SliceData.Builder builder = new SliceData.Builder() diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java index a4020dd3d31..52c4e420475 100644 --- a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseHelperTest.java @@ -1,7 +1,26 @@ +/* + * Copyright (C) 2017 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.slices; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -19,6 +38,8 @@ import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import java.util.Locale; + @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class SlicesDatabaseHelperTest { @@ -30,7 +51,7 @@ public class SlicesDatabaseHelperTest { @Before public void setUp() { mContext = RuntimeEnvironment.application; - mSlicesDatabaseHelper = new SlicesDatabaseHelper(mContext); + mSlicesDatabaseHelper = spy(SlicesDatabaseHelper.getInstance(mContext)); mDatabase = mSlicesDatabaseHelper.getWritableDatabase(); } @@ -47,7 +68,7 @@ public class SlicesDatabaseHelperTest { String[] expectedNames = new String[]{ IndexColumns.KEY, IndexColumns.TITLE, - IndexColumns.SUBTITLE, + IndexColumns.SUMMARY, IndexColumns.SCREENTITLE, IndexColumns.ICON_RESOURCE, IndexColumns.FRAGMENT, @@ -71,17 +92,48 @@ public class SlicesDatabaseHelperTest { assertThat(newCursor.getCount()).isEqualTo(0); } + @Test + public void testIndexState_buildAndLocaleSet() { + mSlicesDatabaseHelper.reconstruct(mDatabase); + + boolean baseState = mSlicesDatabaseHelper.isSliceDataIndexed(); + assertThat(baseState).isFalse(); + + mSlicesDatabaseHelper.setIndexedState(); + boolean indexedState = mSlicesDatabaseHelper.isSliceDataIndexed(); + assertThat(indexedState).isTrue(); + } + + @Test + public void testLocaleChanges_newIndexingState() { + mSlicesDatabaseHelper.reconstruct(mDatabase); + mSlicesDatabaseHelper.setIndexedState(); + + Locale.setDefault(new Locale("ca")); + + assertThat(mSlicesDatabaseHelper.isSliceDataIndexed()).isFalse(); + } + + @Test + public void testBuildFingerprintChanges_newIndexingState() { + mSlicesDatabaseHelper.reconstruct(mDatabase); + mSlicesDatabaseHelper.setIndexedState(); + doReturn("newBuild").when(mSlicesDatabaseHelper).getBuildTag(); + + assertThat(mSlicesDatabaseHelper.isSliceDataIndexed()).isFalse(); + } + private ContentValues getDummyRow() { ContentValues values; values = new ContentValues(); values.put(IndexColumns.KEY, "key"); values.put(IndexColumns.TITLE, "title"); - values.put(IndexColumns.SUBTITLE, "subtitle"); + values.put(IndexColumns.SUMMARY, "summary"); values.put(IndexColumns.ICON_RESOURCE, 99); values.put(IndexColumns.FRAGMENT, "fragmentClassName"); values.put(IndexColumns.CONTROLLER, "preferenceController"); return values; } -} +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SlicesIndexerTest.java b/tests/robotests/src/com/android/settings/slices/SlicesIndexerTest.java new file mode 100644 index 00000000000..68c95558d99 --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SlicesIndexerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017 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.slices; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; +import com.android.settings.testutils.DatabaseTestUtils; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SlicesIndexerTest { + + private final String[] KEYS = new String[]{"key1", "key2", "key3"}; + private final String[] TITLES = new String[]{"title1", "title2", "title3"}; + private final String SUMMARY = "subtitle"; + private final String SCREEN_TITLE = "screen title"; + private final String FRAGMENT_NAME = "fragment name"; + private final int ICON = 1234; // I declare a thumb war + private final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + private final String PREF_CONTROLLER = "com.android.settings.slices.tester"; + + private Context mContext; + + private SlicesIndexer mManager; + + private SQLiteDatabase mDb; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mManager = spy(new SlicesIndexer(mContext)); + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + } + + @Test + public void testAlreadyIndexed_doesNotIndexAgain() { + String newKey = "newKey"; + String newTitle = "newTitle"; + SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); + Locale.setDefault(new Locale("ca")); + insertSpecialCase(newKey, newTitle); + + // Attempt indexing - should not do anything. + mManager.run(); + + Cursor cursor = mDb.rawQuery("SELECT * FROM slices_index", null); + cursor.moveToFirst(); + assertThat(cursor.getCount()).isEqualTo(1); + assertThat(cursor.getString(cursor.getColumnIndex(IndexColumns.KEY))).isEqualTo(newKey); + assertThat(cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE))).isEqualTo(newTitle); + } + + @Test + public void testInsertSliceData_indexedStateSet() { + SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); + helper.setIndexedState(); + doReturn(new ArrayList()).when(mManager).getSliceData(); + + mManager.run(); + + assertThat(helper.isSliceDataIndexed()).isTrue(); + } + + @Test + public void testInsertSliceData_mockDataInserted() { + List sliceData = getDummyIndexableData(); + doReturn(sliceData).when(mManager).getSliceData(); + + mManager.run(); + + Cursor cursor = mDb.rawQuery("SELECT * FROM slices_index", null); + assertThat(cursor.getCount()).isEqualTo(sliceData.size()); + + cursor.moveToFirst(); + for (int i = 0; i < sliceData.size(); i++) { + assertThat(cursor.getString(cursor.getColumnIndex(IndexColumns.KEY))).isEqualTo( + KEYS[i]); + assertThat(cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE))).isEqualTo( + TITLES[i]); + cursor.moveToNext(); + } + } + + private void insertSpecialCase(String key, String title) { + ContentValues values = new ContentValues(); + values.put(IndexColumns.KEY, key); + values.put(IndexColumns.TITLE, title); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } + + private List getDummyIndexableData() { + List sliceData = new ArrayList<>(); + SliceData.Builder builder = new SliceData.Builder(); + builder.setSummary(SUMMARY) + .setScreenTitle(SCREEN_TITLE) + .setFragmentName(FRAGMENT_NAME) + .setIcon(ICON) + .setUri(URI) + .setPreferenceControllerClassName(PREF_CONTROLLER); + + for (int i = 0; i < KEYS.length; i++) { + builder.setKey(KEYS[i]) + .setTitle(TITLES[i]); + sliceData.add(builder.build()); + } + + return sliceData; + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java b/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java index 7472996e036..11e740ab0ae 100644 --- a/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/DatabaseTestUtils.java @@ -19,12 +19,33 @@ package com.android.settings.testutils; import android.content.Context; import com.android.settings.search.IndexDatabaseHelper; +import com.android.settings.slices.SlicesDatabaseHelper; import java.lang.reflect.Field; public class DatabaseTestUtils { public static void clearDb(Context context) { + clearSearchDb(context); + clearSlicesDb(context); + } + + private static void clearSlicesDb(Context context) { + SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(context); + helper.close(); + + Field instance; + Class clazz = SlicesDatabaseHelper.class; + try { + instance = clazz.getDeclaredField("sSingleton"); + instance.setAccessible(true); + instance.set(null, null); + } catch (Exception e) { + throw new RuntimeException(); + } + } + + private static void clearSearchDb(Context context) { IndexDatabaseHelper helper = IndexDatabaseHelper.getInstance(context); helper.close(); diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java index 5430e9b11f1..fb2b62e7413 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -37,6 +37,7 @@ import com.android.settings.overlay.SupportFeatureProvider; import com.android.settings.overlay.SurveyFeatureProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.security.SecurityFeatureProvider; +import com.android.settings.slices.SlicesFeatureProvider; import com.android.settings.users.UserFeatureProvider; import org.mockito.Answers; @@ -63,6 +64,7 @@ public class FakeFeatureFactory extends FeatureFactory { public final BluetoothFeatureProvider bluetoothFeatureProvider; public final DataPlanFeatureProvider dataPlanFeatureProvider; public final SmsMirroringFeatureProvider smsMirroringFeatureProvider; + public final SlicesFeatureProvider slicesFeatureProvider; /** * Call this in {@code @Before} method of the test class to use fake factory. @@ -101,6 +103,7 @@ public class FakeFeatureFactory extends FeatureFactory { bluetoothFeatureProvider = mock(BluetoothFeatureProvider.class); dataPlanFeatureProvider = mock(DataPlanFeatureProvider.class); smsMirroringFeatureProvider = mock(SmsMirroringFeatureProvider.class); + slicesFeatureProvider = mock(SlicesFeatureProvider.class); } @Override @@ -182,4 +185,9 @@ public class FakeFeatureFactory extends FeatureFactory { public SmsMirroringFeatureProvider getSmsMirroringFeatureProvider() { return smsMirroringFeatureProvider; } + + @Override + public SlicesFeatureProvider getSlicesFeatureProvider() { + return slicesFeatureProvider; + } }