blob: bd5359609591db4d04cdc5d8fc3b915415378275 [file] [log] [blame]
/*
* 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.intelligence.search.indexing;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.provider.SearchIndexableData;
import android.provider.SearchIndexableResource;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.Xml;
import com.android.settings.intelligence.search.ResultPayload;
import com.android.settings.intelligence.search.SearchFeatureProvider;
import com.android.settings.intelligence.search.SearchIndexableRaw;
import com.android.settings.intelligence.search.sitemap.SiteMapPair;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Helper class to convert {@link PreIndexData} to {@link IndexData}.
*/
public class IndexDataConverter {
private static final String TAG = "IndexDataConverter";
private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
private static final List<String> SKIP_NODES = Arrays.asList("intent", "extra");
private final Context mContext;
public IndexDataConverter(Context context) {
mContext = context;
}
/**
* Return the collection of {@param preIndexData} converted into {@link IndexData}.
*
* @param preIndexData a collection of {@link SearchIndexableResource},
* {@link SearchIndexableRaw} and non-indexable keys.
*/
public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) {
final long startConversion = System.currentTimeMillis();
final Map<String, List<SearchIndexableData>> indexableDataMap =
preIndexData.getDataToUpdate();
final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
final List<IndexData> indexData = new ArrayList<>();
for (Map.Entry<String, List<SearchIndexableData>> entry : indexableDataMap.entrySet()) {
final String authority = entry.getKey();
final List<SearchIndexableData> indexableData = entry.getValue();
for (SearchIndexableData data : indexableData) {
if (data instanceof SearchIndexableRaw) {
final SearchIndexableRaw rawData = (SearchIndexableRaw) data;
final Set<String> rawNonIndexableKeys = nonIndexableKeys.get(authority);
final IndexData convertedRaw = convertRaw(mContext, authority, rawData,
rawNonIndexableKeys);
if (convertedRaw != null) {
indexData.add(convertedRaw);
}
} else if (data instanceof SearchIndexableResource) {
final SearchIndexableResource sir = (SearchIndexableResource) data;
final Set<String> resourceNonIndexableKeys =
getNonIndexableKeysForResource(nonIndexableKeys, authority);
final List<IndexData> resourceData = convertResource(sir, authority,
resourceNonIndexableKeys);
indexData.addAll(resourceData);
}
}
}
final long endConversion = System.currentTimeMillis();
Log.d(TAG, "Converting pre-index data to index data took: "
+ (endConversion - startConversion));
return indexData;
}
/**
* Returns a full list of site map pairs based on metadata from all data sources.
*
* The content schema follows {@link IndexDatabaseHelper.Tables#TABLE_SITE_MAP}
*/
public List<SiteMapPair> convertSiteMapPairs(List<IndexData> indexData,
List<Pair<String, String>> siteMapClassNames) {
final List<SiteMapPair> pairs = new ArrayList<>();
if (indexData == null) {
return pairs;
}
// Step 1: loop indexData and build all static site map pairs.
final Map<String, String> classToTitleMap = new TreeMap<>();
for (IndexData row : indexData) {
if (TextUtils.isEmpty(row.className)) {
continue;
}
// Build a map of [class, title] for the next step.
classToTitleMap.put(row.className, row.screenTitle);
if (!TextUtils.isEmpty(row.childClassName)) {
pairs.add(new SiteMapPair(row.className, row.screenTitle,
row.childClassName, row.updatedTitle));
}
}
// Step 2: Extend the sitemap pairs by adding dynamic pairs provided by
// SearchIndexableProvider. The provider only tells us class name so we need to finish
// the mapping by looking up display title for each class.
for (Pair<String, String> pair : siteMapClassNames) {
final String parentName = classToTitleMap.get(pair.first);
final String childName = classToTitleMap.get(pair.second);
if (TextUtils.isEmpty(parentName) || TextUtils.isEmpty(childName)) {
Log.w(TAG, "Cannot build sitemap pair for incomplete names "
+ pair + parentName + childName);
} else {
pairs.add(new SiteMapPair(pair.first, parentName, pair.second, childName));
}
}
// Done
return pairs;
}
/**
* Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}.
* The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData},
* and there is some data sanitization in the conversion.
*/
@Nullable
private IndexData convertRaw(Context context, String authority, SearchIndexableRaw raw,
Set<String> nonIndexableKeys) {
if (TextUtils.isEmpty(raw.key)) {
Log.w(TAG, "Skipping null key for raw indexable " + authority + "/" + raw.title);
return null;
}
// A row is enabled if it does not show up as an nonIndexableKey
boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key));
final IndexData.Builder builder = new IndexData.Builder();
builder.setTitle(raw.title)
.setSummaryOn(raw.summaryOn)
.setEntries(raw.entries)
.setKeywords(raw.keywords)
.setClassName(raw.className)
.setScreenTitle(raw.screenTitle)
.setIconResId(raw.iconResId)
.setIntentAction(raw.intentAction)
.setIntentTargetPackage(raw.intentTargetPackage)
.setIntentTargetClass(raw.intentTargetClass)
.setEnabled(enabled)
.setPackageName(raw.packageName)
.setAuthority(authority)
.setKey(raw.key);
return builder.build(context);
}
/**
* Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}.
* Each of the elements in the xml layout attribute of {@param sir} is a candidate to be
* converted (including the header element).
*
* TODO (b/33577327) simplify this method.
*/
private List<IndexData> convertResource(SearchIndexableResource sir, String authority,
Set<String> nonIndexableKeys) {
final Context context = sir.context;
XmlResourceParser parser = null;
List<IndexData> resourceIndexData = new ArrayList<>();
try {
parser = context.getResources().getXml(sir.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(context, attrs);
final String headerKey = XmlParserUtils.getDataKey(context, attrs);
String title;
String key;
String headerTitle;
String summary;
String headerSummary;
String keywords;
String headerKeywords;
String childFragment;
@DrawableRes int iconResId;
ResultPayload payload;
boolean enabled;
// TODO REFACTOR (b/62807132) Add proper inline support
// Map<String, PreferenceControllerMixin> controllerUriMap = null;
//
// if (fragmentName != null) {
// controllerUriMap = DatabaseIndexingUtils
// .getPreferenceControllerUriMap(fragmentName, context);
// }
headerTitle = XmlParserUtils.getDataTitle(context, attrs);
headerSummary = XmlParserUtils.getDataSummary(context, attrs);
headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
enabled = !nonIndexableKeys.contains(headerKey);
// TODO: Set payload type for header results
IndexData.Builder headerBuilder = new IndexData.Builder();
headerBuilder.setTitle(headerTitle)
.setSummaryOn(headerSummary)
.setScreenTitle(screenTitle)
.setKeywords(headerKeywords)
.setClassName(sir.className)
.setPackageName(sir.packageName)
.setAuthority(authority)
.setIntentAction(sir.intentAction)
.setIntentTargetPackage(sir.intentTargetPackage)
.setIntentTargetClass(sir.intentTargetClass)
.setEnabled(enabled)
.setKey(headerKey);
// Flag for XML headers which a child element's title.
boolean isHeaderUnique = true;
IndexData.Builder builder;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
nodeName = parser.getName();
if (SKIP_NODES.contains(nodeName)) {
if (SearchFeatureProvider.DEBUG) {
Log.d(TAG, nodeName + " is not a valid entity to index, skip.");
}
continue;
}
title = XmlParserUtils.getDataTitle(context, attrs);
key = XmlParserUtils.getDataKey(context, attrs);
enabled = !nonIndexableKeys.contains(key);
keywords = XmlParserUtils.getDataKeywords(context, attrs);
iconResId = XmlParserUtils.getDataIcon(context, attrs);
if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
isHeaderUnique = false;
}
builder = new IndexData.Builder();
builder.setTitle(title)
.setKeywords(keywords)
.setClassName(sir.className)
.setScreenTitle(screenTitle)
.setIconResId(iconResId)
.setPackageName(sir.packageName)
.setAuthority(authority)
.setIntentAction(sir.intentAction)
.setIntentTargetPackage(sir.intentTargetPackage)
.setIntentTargetClass(sir.intentTargetClass)
.setEnabled(enabled)
.setKey(key);
if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
summary = XmlParserUtils.getDataSummary(context, attrs);
String entries = null;
if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
entries = XmlParserUtils.getDataEntries(context, attrs);
}
// TODO (b/62254931) index primitives instead of payload
// TODO (b/62807132) Add proper inline support
//payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
builder.setSummaryOn(summary)
.setEntries(entries)
.setChildClassName(childFragment);
tryAddIndexDataToList(resourceIndexData, builder);
} else {
// TODO (b/33577327) We removed summary off here. We should check if we can
// merge this 'else' section with the one above. Put a break point to
// investigate.
String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
if (TextUtils.isEmpty(summaryOn)) {
summaryOn = XmlParserUtils.getDataSummary(context, attrs);
}
builder.setSummaryOn(summaryOn);
tryAddIndexDataToList(resourceIndexData, builder);
}
}
// The xml header's title does not match the title of one of the child settings.
if (isHeaderUnique) {
tryAddIndexDataToList(resourceIndexData, headerBuilder);
}
} catch (XmlPullParserException e) {
Log.w(TAG, "XML Error parsing PreferenceScreen: " + sir.className, e);
} catch (IOException e) {
Log.w(TAG, "IO Error parsing PreferenceScreen: " + sir.className, e);
} catch (Resources.NotFoundException e) {
Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: " + sir.className, e);
} finally {
if (parser != null) {
parser.close();
}
}
return resourceIndexData;
}
private void tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data) {
if (!TextUtils.isEmpty(data.getKey())) {
list.add(data.build(mContext));
} else {
Log.w(TAG, "Skipping index for null-key item " + data);
}
}
private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys,
String authority) {
final Set<String> result = nonIndexableKeys.get(authority);
return result != null ? result : new ArraySet<>();
}
}