| /* |
| * 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_ALLOW_DYNAMIC_SUMMARY_IN_SLICE; |
| 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 static com.android.settings.core.PreferenceXmlParserUtils.METADATA_UNAVAILABLE_SLICE_SUBTITLE; |
| |
| 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 androidx.annotation.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 |
| | MetadataFlag.FLAG_ALLOW_DYNAMIC_SUMMARY_IN_SLICE |
| | MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE); |
| |
| 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 boolean isDynamicSummaryAllowed = bundle.getBoolean( |
| METADATA_ALLOW_DYNAMIC_SUMMARY_IN_SLICE); |
| final String unavailableSliceSubtitle = bundle.getString( |
| METADATA_UNAVAILABLE_SLICE_SUBTITLE); |
| |
| final SliceData xmlSlice = new SliceData.Builder() |
| .setKey(key) |
| .setTitle(title) |
| .setSummary(summary) |
| .setIcon(iconResId) |
| .setScreenTitle(screenTitle) |
| .setPreferenceControllerClassName(controllerClassName) |
| .setFragmentName(fragmentName) |
| .setSliceType(sliceType) |
| .setPlatformDefined(isPlatformSlice) |
| .setDynamicSummaryAllowed(isDynamicSummaryAllowed) |
| .setUnavailableSliceSubtitle(unavailableSliceSubtitle) |
| .build(); |
| |
| final BasePreferenceController controller = |
| SliceBuilderUtils.getPreferenceController(mContext, xmlSlice); |
| |
| // Only add pre-approved Slices available on the device. |
| if (controller.isSliceable() && controller.isAvailable()) { |
| 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.drawable.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(); |
| } |
| } |