| /* |
| * 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.os.AsyncTask; |
| import android.provider.SearchIndexableData; |
| import android.provider.SearchIndexableResource; |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.Nullable; |
| 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.HashSet; |
| 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 List<SearchIndexableData> indexableData = preIndexData.getDataToUpdate(); |
| final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys(); |
| final List<IndexData> indexData = new ArrayList<>(); |
| |
| for (SearchIndexableData data : indexableData) { |
| if (data instanceof SearchIndexableRaw) { |
| final SearchIndexableRaw rawData = (SearchIndexableRaw) data; |
| final Set<String> rawNonIndexableKeys = nonIndexableKeys.get( |
| rawData.intentTargetPackage); |
| final IndexData convertedRaw = convertRaw(mContext, rawData, rawNonIndexableKeys); |
| if (convertedRaw != null) { |
| indexData.add(convertedRaw); |
| } |
| } else if (data instanceof SearchIndexableResource) { |
| final SearchIndexableResource sir = (SearchIndexableResource) data; |
| final Set<String> resourceNonIndexableKeys = |
| getNonIndexableKeysForResource(nonIndexableKeys, sir.packageName); |
| final List<IndexData> resourceData = convertResource(sir, 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, SearchIndexableRaw raw, |
| Set<String> nonIndexableKeys) { |
| if (TextUtils.isEmpty(raw.key)) { |
| Log.w(TAG, "Skipping null key for raw indexable " + raw.packageName + "/" + 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) |
| .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, |
| 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) |
| .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) |
| .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 packageName) { |
| return nonIndexableKeys.containsKey(packageName) |
| ? nonIndexableKeys.get(packageName) |
| : new HashSet<String>(); |
| } |
| } |