1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.settings.slices; 18 19 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_CONTROLLER; 20 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_ICON; 21 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY; 22 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFORM_SLICE_FLAG; 23 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY; 24 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE; 25 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_UNAVAILABLE_SLICE_SUBTITLE; 26 27 import android.accessibilityservice.AccessibilityServiceInfo; 28 import android.app.settings.SettingsEnums; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.pm.ServiceInfo; 34 import android.content.res.Resources; 35 import android.content.res.XmlResourceParser; 36 import android.os.Bundle; 37 import android.provider.SearchIndexableResource; 38 import android.text.TextUtils; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.util.Xml; 42 import android.view.accessibility.AccessibilityManager; 43 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.settings.R; 47 import com.android.settings.accessibility.AccessibilitySettings; 48 import com.android.settings.accessibility.AccessibilitySlicePreferenceController; 49 import com.android.settings.core.BasePreferenceController; 50 import com.android.settings.core.PreferenceXmlParserUtils; 51 import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; 52 import com.android.settings.dashboard.DashboardFragment; 53 import com.android.settings.overlay.FeatureFactory; 54 import com.android.settings.search.DatabaseIndexingUtils; 55 import com.android.settings.search.Indexable.SearchIndexProvider; 56 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 57 58 import org.xmlpull.v1.XmlPullParser; 59 import org.xmlpull.v1.XmlPullParserException; 60 61 import java.io.IOException; 62 import java.util.ArrayList; 63 import java.util.Collection; 64 import java.util.Collections; 65 import java.util.HashSet; 66 import java.util.List; 67 import java.util.Set; 68 69 /** 70 * Converts all Slice sources into {@link SliceData}. 71 * This includes: 72 * - All {@link DashboardFragment DashboardFragments} indexed by settings search 73 * - Accessibility services 74 */ 75 class SliceDataConverter { 76 77 private static final String TAG = "SliceDataConverter"; 78 79 private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; 80 81 private final MetricsFeatureProvider mMetricsFeatureProvider; 82 private Context mContext; 83 SliceDataConverter(Context context)84 public SliceDataConverter(Context context) { 85 mContext = context; 86 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 87 } 88 89 /** 90 * @return a list of {@link SliceData} to be indexed and later referenced as a Slice. 91 * 92 * The collection works as follows: 93 * - Collects a list of Fragments from 94 * {@link FeatureFactory#getSearchFeatureProvider()}. 95 * - From each fragment, grab a {@link SearchIndexProvider}. 96 * - For each provider, collect XML resource layout and a list of 97 * {@link com.android.settings.core.BasePreferenceController}. 98 */ getSliceData()99 public List<SliceData> getSliceData() { 100 List<SliceData> sliceData = new ArrayList<>(); 101 102 final Collection<Class> indexableClasses = FeatureFactory.getFactory(mContext) 103 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues(); 104 105 for (Class clazz : indexableClasses) { 106 final String fragmentName = clazz.getName(); 107 108 final SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider( 109 clazz); 110 111 // CodeInspection test guards against the null check. Keep check in case of bad actors. 112 if (provider == null) { 113 Log.e(TAG, fragmentName + " dose not implement Search Index Provider"); 114 continue; 115 } 116 117 final List<SliceData> providerSliceData = getSliceDataFromProvider(provider, 118 fragmentName); 119 sliceData.addAll(providerSliceData); 120 } 121 122 final List<SliceData> a11ySliceData = getAccessibilitySliceData(); 123 sliceData.addAll(a11ySliceData); 124 return sliceData; 125 } 126 getSliceDataFromProvider(SearchIndexProvider provider, String fragmentName)127 private List<SliceData> getSliceDataFromProvider(SearchIndexProvider provider, 128 String fragmentName) { 129 final List<SliceData> sliceData = new ArrayList<>(); 130 131 final List<SearchIndexableResource> resList = 132 provider.getXmlResourcesToIndex(mContext, true /* enabled */); 133 134 if (resList == null) { 135 return sliceData; 136 } 137 138 // TODO (b/67996923) get a list of permanent NIKs and skip the invalid keys. 139 140 for (SearchIndexableResource resource : resList) { 141 int xmlResId = resource.xmlResId; 142 if (xmlResId == 0) { 143 Log.e(TAG, fragmentName + " provides invalid XML (0) in search provider."); 144 continue; 145 } 146 147 List<SliceData> xmlSliceData = getSliceDataFromXML(xmlResId, fragmentName); 148 sliceData.addAll(xmlSliceData); 149 } 150 151 return sliceData; 152 } 153 getSliceDataFromXML(int xmlResId, String fragmentName)154 private List<SliceData> getSliceDataFromXML(int xmlResId, String fragmentName) { 155 XmlResourceParser parser = null; 156 157 final List<SliceData> xmlSliceData = new ArrayList<>(); 158 String controllerClassName = ""; 159 160 try { 161 parser = mContext.getResources().getXml(xmlResId); 162 163 int type; 164 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 165 && type != XmlPullParser.START_TAG) { 166 // Parse next until start tag is found 167 } 168 169 String nodeName = parser.getName(); 170 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 171 throw new RuntimeException( 172 "XML document must start with <PreferenceScreen> tag; found" 173 + nodeName + " at " + parser.getPositionDescription()); 174 } 175 176 final AttributeSet attrs = Xml.asAttributeSet(parser); 177 final String screenTitle = PreferenceXmlParserUtils.getDataTitle(mContext, attrs); 178 179 // TODO (b/67996923) Investigate if we need headers for Slices, since they never 180 // correspond to an actual setting. 181 182 final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext, 183 xmlResId, 184 MetadataFlag.FLAG_NEED_KEY 185 | MetadataFlag.FLAG_NEED_PREF_CONTROLLER 186 | MetadataFlag.FLAG_NEED_PREF_TYPE 187 | MetadataFlag.FLAG_NEED_PREF_TITLE 188 | MetadataFlag.FLAG_NEED_PREF_ICON 189 | MetadataFlag.FLAG_NEED_PREF_SUMMARY 190 | MetadataFlag.FLAG_NEED_PLATFORM_SLICE_FLAG 191 | MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE); 192 193 for (Bundle bundle : metadata) { 194 // TODO (b/67996923) Non-controller Slices should become intent-only slices. 195 // Note that without a controller, dynamic summaries are impossible. 196 controllerClassName = bundle.getString(METADATA_CONTROLLER); 197 if (TextUtils.isEmpty(controllerClassName)) { 198 continue; 199 } 200 201 final String key = bundle.getString(METADATA_KEY); 202 final String title = bundle.getString(METADATA_TITLE); 203 final String summary = bundle.getString(METADATA_SUMMARY); 204 final int iconResId = bundle.getInt(METADATA_ICON); 205 final int sliceType = SliceBuilderUtils.getSliceType(mContext, controllerClassName, 206 key); 207 final boolean isPlatformSlice = bundle.getBoolean(METADATA_PLATFORM_SLICE_FLAG); 208 final String unavailableSliceSubtitle = bundle.getString( 209 METADATA_UNAVAILABLE_SLICE_SUBTITLE); 210 211 final SliceData xmlSlice = new SliceData.Builder() 212 .setKey(key) 213 .setTitle(title) 214 .setSummary(summary) 215 .setIcon(iconResId) 216 .setScreenTitle(screenTitle) 217 .setPreferenceControllerClassName(controllerClassName) 218 .setFragmentName(fragmentName) 219 .setSliceType(sliceType) 220 .setPlatformDefined(isPlatformSlice) 221 .setUnavailableSliceSubtitle(unavailableSliceSubtitle) 222 .build(); 223 224 final BasePreferenceController controller = 225 SliceBuilderUtils.getPreferenceController(mContext, xmlSlice); 226 227 // Only add pre-approved Slices available on the device. 228 if (controller.isSliceable() && controller.isAvailable()) { 229 xmlSliceData.add(xmlSlice); 230 } 231 } 232 } catch (SliceData.InvalidSliceDataException e) { 233 Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e); 234 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 235 SettingsEnums.ACTION_VERIFY_SLICE_ERROR_INVALID_DATA, 236 SettingsEnums.PAGE_UNKNOWN, 237 controllerClassName, 238 1); 239 } catch (XmlPullParserException | IOException | Resources.NotFoundException e) { 240 Log.w(TAG, "Error parsing PreferenceScreen: ", e); 241 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 242 SettingsEnums.ACTION_VERIFY_SLICE_PARSING_ERROR, 243 SettingsEnums.PAGE_UNKNOWN, 244 fragmentName, 245 1); 246 } catch (Exception e) { 247 Log.w(TAG, "Get slice data from XML failed ", e); 248 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 249 SettingsEnums.ACTION_VERIFY_SLICE_OTHER_EXCEPTION, 250 SettingsEnums.PAGE_UNKNOWN, 251 fragmentName + "_" + controllerClassName, 252 1); 253 } finally { 254 if (parser != null) parser.close(); 255 } 256 return xmlSliceData; 257 } 258 getAccessibilitySliceData()259 private List<SliceData> getAccessibilitySliceData() { 260 final List<SliceData> sliceData = new ArrayList<>(); 261 262 final String accessibilityControllerClassName = 263 AccessibilitySlicePreferenceController.class.getName(); 264 final String fragmentClassName = AccessibilitySettings.class.getName(); 265 final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings); 266 267 final SliceData.Builder sliceDataBuilder = new SliceData.Builder() 268 .setFragmentName(fragmentClassName) 269 .setScreenTitle(screenTitle) 270 .setPreferenceControllerClassName(accessibilityControllerClassName); 271 272 final Set<String> a11yServiceNames = new HashSet<>(); 273 Collections.addAll(a11yServiceNames, mContext.getResources() 274 .getStringArray(R.array.config_settings_slices_accessibility_components)); 275 final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList(); 276 final PackageManager packageManager = mContext.getPackageManager(); 277 278 for (AccessibilityServiceInfo a11yServiceInfo : installedServices) { 279 final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo(); 280 final ServiceInfo serviceInfo = resolveInfo.serviceInfo; 281 final String packageName = serviceInfo.packageName; 282 final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); 283 final String flattenedName = componentName.flattenToString(); 284 285 if (!a11yServiceNames.contains(flattenedName)) { 286 continue; 287 } 288 289 final String title = resolveInfo.loadLabel(packageManager).toString(); 290 int iconResource = resolveInfo.getIconResource(); 291 if (iconResource == 0) { 292 iconResource = R.drawable.ic_accessibility_generic; 293 } 294 295 sliceDataBuilder.setKey(flattenedName) 296 .setTitle(title) 297 .setIcon(iconResource) 298 .setSliceType(SliceData.SliceType.SWITCH); 299 try { 300 sliceData.add(sliceDataBuilder.build()); 301 } catch (SliceData.InvalidSliceDataException e) { 302 Log.w(TAG, "Invalid data when building a11y SliceData for " + flattenedName, e); 303 } 304 } 305 306 return sliceData; 307 } 308 309 @VisibleForTesting getAccessibilityServiceInfoList()310 List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() { 311 final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance( 312 mContext); 313 return accessibilityManager.getInstalledAccessibilityServiceList(); 314 } 315 }