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; + } }