Files
packages_apps_Settings/src/com/android/settings/slices/SliceDataConverter.java
Matthew Fritze bf1f5b5813 Add isSliceable API to BasePrefController
Only support explicitly approved Settings Slices,
dictated by controllers which return true for the new
method isSliceable.

Updating the supported settings to a whitelist means that
the method to return all available slices must be updated,
and checking slicability when we index slices.

Test: robotests
Bug: 79779103
Change-Id: Ib2b9690cdd0036b5cc4a1cb846c52bce7c824ab9
2018-05-22 11:06:42 -07:00

296 lines
12 KiB
Java

/*
* 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.android.settings.core.PreferenceXmlParserUtils.METADATA_CONTROLLER;
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_ICON;
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY;
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFORM_SLICE_FLAG;
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY;
import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.provider.SearchIndexableResource;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.accessibility.AccessibilitySettings;
import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceXmlParserUtils;
import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.DatabaseIndexingUtils;
import com.android.settings.search.Indexable.SearchIndexProvider;
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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Converts all Slice sources into {@link SliceData}.
* This includes:
* - All {@link DashboardFragment DashboardFragments} indexed by settings search
* - Accessibility services
*/
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 FeatureFactory#getSearchFeatureProvider()}.
* - 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 = FeatureFactory.getFactory(mContext)
.getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
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);
}
final List<SliceData> a11ySliceData = getAccessibilitySliceData();
mSliceData.addAll(a11ySliceData);
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<>();
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 AttributeSet attrs = Xml.asAttributeSet(parser);
final String screenTitle = PreferenceXmlParserUtils.getDataTitle(mContext, attrs);
// TODO (b/67996923) Investigate if we need headers for Slices, since they never
// correspond to an actual setting.
final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext,
xmlResId,
MetadataFlag.FLAG_NEED_KEY
| MetadataFlag.FLAG_NEED_PREF_CONTROLLER
| MetadataFlag.FLAG_NEED_PREF_TYPE
| MetadataFlag.FLAG_NEED_PREF_TITLE
| MetadataFlag.FLAG_NEED_PREF_ICON
| MetadataFlag.FLAG_NEED_PREF_SUMMARY
| MetadataFlag.FLAG_NEED_PLATFORM_SLICE_FLAG);
for (Bundle bundle : metadata) {
// TODO (b/67996923) Non-controller Slices should become intent-only slices.
// Note that without a controller, dynamic summaries are impossible.
final String controllerClassName = bundle.getString(METADATA_CONTROLLER);
if (TextUtils.isEmpty(controllerClassName)) {
continue;
}
final String key = bundle.getString(METADATA_KEY);
final String title = bundle.getString(METADATA_TITLE);
final String summary = bundle.getString(METADATA_SUMMARY);
final int iconResId = bundle.getInt(METADATA_ICON);
final int sliceType = SliceBuilderUtils.getSliceType(mContext, controllerClassName,
key);
final boolean isPlatformSlice = bundle.getBoolean(METADATA_PLATFORM_SLICE_FLAG);
final SliceData xmlSlice = new SliceData.Builder()
.setKey(key)
.setTitle(title)
.setSummary(summary)
.setIcon(iconResId)
.setScreenTitle(screenTitle)
.setPreferenceControllerClassName(controllerClassName)
.setFragmentName(fragmentName)
.setSliceType(sliceType)
.setPlatformDefined(isPlatformSlice)
.build();
final BasePreferenceController controller =
SliceBuilderUtils.getPreferenceController(mContext, xmlSlice);
// Only add pre-approved Slices available on the device.
if (controller.isAvailable() && controller.isSliceable()) {
xmlSliceData.add(xmlSlice);
}
}
} catch (SliceData.InvalidSliceDataException e) {
Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e);
} 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, "Resource not found error parsing PreferenceScreen: ", e);
} finally {
if (parser != null) parser.close();
}
return xmlSliceData;
}
private List<SliceData> getAccessibilitySliceData() {
final List<SliceData> sliceData = new ArrayList<>();
final String accessibilityControllerClassName =
AccessibilitySlicePreferenceController.class.getName();
final String fragmentClassName = AccessibilitySettings.class.getName();
final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings);
final SliceData.Builder sliceDataBuilder = new SliceData.Builder()
.setFragmentName(fragmentClassName)
.setScreenTitle(screenTitle)
.setPreferenceControllerClassName(accessibilityControllerClassName);
final Set<String> a11yServiceNames = new HashSet<>();
Collections.addAll(a11yServiceNames, mContext.getResources()
.getStringArray(R.array.config_settings_slices_accessibility_components));
final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList();
final PackageManager packageManager = mContext.getPackageManager();
for (AccessibilityServiceInfo a11yServiceInfo : installedServices) {
final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo();
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
final String packageName = serviceInfo.packageName;
final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
final String flattenedName = componentName.flattenToString();
if (!a11yServiceNames.contains(flattenedName)) {
continue;
}
final String title = resolveInfo.loadLabel(packageManager).toString();
int iconResource = resolveInfo.getIconResource();
if (iconResource == 0) {
iconResource = R.mipmap.ic_accessibility_generic;
}
sliceDataBuilder.setKey(flattenedName)
.setTitle(title)
.setIcon(iconResource)
.setSliceType(SliceData.SliceType.SWITCH);
try {
sliceData.add(sliceDataBuilder.build());
} catch (SliceData.InvalidSliceDataException e) {
Log.w(TAG, "Invalid data when building a11y SliceData for " + flattenedName, e);
}
}
return sliceData;
}
@VisibleForTesting
List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() {
final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
mContext);
return accessibilityManager.getInstalledAccessibilityServiceList();
}
}