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

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