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
This commit is contained in:
Matthew Fritze
2017-12-12 16:03:22 -08:00
parent ce2b7fe988
commit 13c43f1900
18 changed files with 882 additions and 34 deletions

View File

@@ -25,8 +25,7 @@
<Preference
android:key="brightness"
android:title="@string/brightness"
settings:keywords="@string/keywords_display_brightness_level"
settings:controller="com.android.settings.display.AutoBrightnessPreferenceController">
settings:keywords="@string/keywords_display_brightness_level">
<intent android:action="com.android.intent.action.SHOW_BRIGHTNESS_DIALOG" />
</Preference>
@@ -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" />
<com.android.settingslib.RestrictedPreference
android:key="wallpaper"

View File

@@ -34,6 +34,7 @@ import com.android.settings.gestures.AssistGestureFeatureProvider;
import com.android.settings.localepicker.LocaleFeatureProvider;
import com.android.settings.security.SecurityFeatureProvider;
import com.android.settings.search.SearchFeatureProvider;
import com.android.settings.slices.SlicesFeatureProvider;
import com.android.settings.users.UserFeatureProvider;
/**
@@ -106,6 +107,8 @@ public abstract class FeatureFactory {
public abstract SmsMirroringFeatureProvider getSmsMirroringFeatureProvider();
public abstract SlicesFeatureProvider getSlicesFeatureProvider();
public static final class FactoryNotFoundException extends RuntimeException {
public FactoryNotFoundException(Throwable throwable) {
super("Unable to create factory. Did you misconfigure Proguard?", throwable);

View File

@@ -48,6 +48,8 @@ import com.android.settings.search.SearchFeatureProvider;
import com.android.settings.search.SearchFeatureProviderImpl;
import com.android.settings.security.SecurityFeatureProvider;
import com.android.settings.security.SecurityFeatureProviderImpl;
import com.android.settings.slices.SlicesFeatureProvider;
import com.android.settings.slices.SlicesFeatureProviderImpl;
import com.android.settings.users.UserFeatureProvider;
import com.android.settings.users.UserFeatureProviderImpl;
import com.android.settings.wrapper.ConnectivityManagerWrapper;
@@ -75,6 +77,7 @@ public class FeatureFactoryImpl extends FeatureFactory {
private BluetoothFeatureProvider mBluetoothFeatureProvider;
private DataPlanFeatureProvider mDataPlanFeatureProvider;
private SmsMirroringFeatureProvider mSmsMirroringFeatureProvider;
private SlicesFeatureProvider mSlicesFeatureProvider;
@Override
public SupportFeatureProvider getSupportFeatureProvider(Context context) {
@@ -208,4 +211,12 @@ public class FeatureFactoryImpl extends FeatureFactory {
}
return mSmsMirroringFeatureProvider;
}
@Override
public SlicesFeatureProvider getSlicesFeatureProvider() {
if (mSlicesFeatureProvider == null) {
mSlicesFeatureProvider = new SlicesFeatureProviderImpl();
}
return mSlicesFeatureProvider;
}
}

View File

@@ -20,8 +20,7 @@ import android.net.Uri;
import android.text.TextUtils;
/**
* TODO (b/67996923) Add SlicesIndexingManager
* Data class representing a slice stored by {@link SlicesIndexingManager}.
* Data class representing a slice stored by {@link SlicesIndexer}.
* Note that {@link #key} is treated as a primary key for this class and determines equality.
*/
public class SliceData {
@@ -173,10 +172,6 @@ public class SliceData {
throw new IllegalStateException("Preference Controller cannot be empty");
}
if (mUri == null) {
throw new IllegalStateException("Uri cannot be null");
}
return new SliceData(this);
}

View File

@@ -0,0 +1,202 @@
/*
* 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.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.provider.SearchIndexableResource;
import android.support.annotation.DrawableRes;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.DatabaseIndexingUtils;
import com.android.settings.search.Indexable.SearchIndexProvider;
import com.android.settings.search.SearchIndexableResources;
import com.android.settings.search.XmlParserUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Converts {@link DashboardFragment} to {@link SliceData}.
*/
class SliceDataConverter {
private static final String TAG = "SliceDataConverter";
private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
private Context mContext;
private List<SliceData> 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<SliceData> getSliceData() {
if (!mSliceData.isEmpty()) {
return mSliceData;
}
final Collection<Class> 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<SliceData> providerSliceData = getSliceDataFromProvider(provider,
fragmentName);
mSliceData.addAll(providerSliceData);
}
return mSliceData;
}
private List<SliceData> getSliceDataFromProvider(SearchIndexProvider provider,
String fragmentName) {
final List<SliceData> sliceData = new ArrayList<>();
final List<SearchIndexableResource> 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<SliceData> xmlSliceData = getSliceDataFromXML(xmlResId, fragmentName);
sliceData.addAll(xmlSliceData);
}
return sliceData;
}
private List<SliceData> getSliceDataFromXML(int xmlResId, String fragmentName) {
XmlResourceParser parser = null;
final List<SliceData> 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 <PreferenceScreen> 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:
// <pref ....>
// <intent action="blab"/> </pref>
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;
}
}

View File

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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<SliceData> indexData = getSliceData();
insertSliceData(database, indexData);
mHelper.setIndexedState();
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
@VisibleForTesting
List<SliceData> getSliceData() {
return FeatureFactory.getFactory(mContext)
.getSlicesFeatureProvider()
.getSliceDataConverter(mContext)
.getSliceData();
}
@VisibleForTesting
void insertSliceData(SQLiteDatabase database, List<SliceData> 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);
}
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 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.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="fake_title_key"
android:title="screen_title">
<Preference
android:key="key"
android:title="title"
android:icon="@drawable/ic_android"
android:summary="summary"
settings:controller="com.android.settings.slices.FakePreferenceController"/>
</PreferenceScreen>

View File

@@ -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<SearchIndexableResource> getXmlResourcesToIndex(Context context,
boolean enabled) {
return null;
List<SearchIndexableResource> 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<AbstractPreferenceController> getPreferenceControllers(Context context) {
public List<AbstractPreferenceController> getPreferenceControllers(
Context context) {
return null;
}
};

View File

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

View File

@@ -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<Class> 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<SliceData> 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);
}
}

View File

@@ -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()

View File

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

View File

@@ -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<SliceData>()).when(mManager).getSliceData();
mManager.run();
assertThat(helper.isSliceDataIndexed()).isTrue();
}
@Test
public void testInsertSliceData_mockDataInserted() {
List<SliceData> 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<SliceData> getDummyIndexableData() {
List<SliceData> 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;
}
}

View File

@@ -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();

View File

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