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_SUMMARY; 23 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE; 24 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_UNAVAILABLE_SLICE_SUBTITLE; 25 26 import android.accessibilityservice.AccessibilityServiceInfo; 27 import android.app.settings.SettingsEnums; 28 import android.content.ComponentName; 29 import android.content.ContentResolver; 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.net.Uri; 37 import android.os.Bundle; 38 import android.provider.SearchIndexableResource; 39 import android.provider.SettingsSlicesContract; 40 import android.text.TextUtils; 41 import android.util.AttributeSet; 42 import android.util.Log; 43 import android.util.Xml; 44 import android.view.accessibility.AccessibilityManager; 45 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.settings.R; 49 import com.android.settings.accessibility.AccessibilitySettings; 50 import com.android.settings.accessibility.AccessibilitySlicePreferenceController; 51 import com.android.settings.core.BasePreferenceController; 52 import com.android.settings.core.PreferenceXmlParserUtils; 53 import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; 54 import com.android.settings.dashboard.DashboardFragment; 55 import com.android.settings.overlay.FeatureFactory; 56 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 57 import com.android.settingslib.search.Indexable.SearchIndexProvider; 58 import com.android.settingslib.search.SearchIndexableData; 59 60 import org.xmlpull.v1.XmlPullParser; 61 import org.xmlpull.v1.XmlPullParserException; 62 63 import java.io.IOException; 64 import java.util.ArrayList; 65 import java.util.Collection; 66 import java.util.Collections; 67 import java.util.HashSet; 68 import java.util.List; 69 import java.util.Set; 70 71 /** 72 * Converts all Slice sources into {@link SliceData}. 73 * This includes: 74 * - All {@link DashboardFragment DashboardFragments} indexed by settings search 75 * - Accessibility services 76 */ 77 class SliceDataConverter { 78 79 private static final String TAG = "SliceDataConverter"; 80 81 private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; 82 83 private final MetricsFeatureProvider mMetricsFeatureProvider; 84 private Context mContext; 85 SliceDataConverter(Context context)86 public SliceDataConverter(Context context) { 87 mContext = context; 88 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 89 } 90 91 /** 92 * @return a list of {@link SliceData} to be indexed and later referenced as a Slice. 93 * 94 * The collection works as follows: 95 * - Collects a list of Fragments from 96 * {@link FeatureFactory#getSearchFeatureProvider()}. 97 * - From each fragment, grab a {@link SearchIndexProvider}. 98 * - For each provider, collect XML resource layout and a list of 99 * {@link com.android.settings.core.BasePreferenceController}. 100 */ getSliceData()101 public List<SliceData> getSliceData() { 102 List<SliceData> sliceData = new ArrayList<>(); 103 104 final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(mContext) 105 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues(); 106 107 for (SearchIndexableData bundle : bundles) { 108 final String fragmentName = bundle.getTargetClass().getName(); 109 110 final SearchIndexProvider provider = bundle.getSearchIndexProvider(); 111 112 // CodeInspection test guards against the null check. Keep check in case of bad actors. 113 if (provider == null) { 114 Log.e(TAG, fragmentName + " dose not implement Search Index Provider"); 115 continue; 116 } 117 118 final List<SliceData> providerSliceData = getSliceDataFromProvider(provider, 119 fragmentName); 120 sliceData.addAll(providerSliceData); 121 } 122 123 final List<SliceData> a11ySliceData = getAccessibilitySliceData(); 124 sliceData.addAll(a11ySliceData); 125 return sliceData; 126 } 127 getSliceDataFromProvider(SearchIndexProvider provider, String fragmentName)128 private List<SliceData> getSliceDataFromProvider(SearchIndexProvider provider, 129 String fragmentName) { 130 final List<SliceData> sliceData = new ArrayList<>(); 131 132 final List<SearchIndexableResource> resList = 133 provider.getXmlResourcesToIndex(mContext, true /* enabled */); 134 135 if (resList == null) { 136 return sliceData; 137 } 138 139 // TODO (b/67996923) get a list of permanent NIKs and skip the invalid keys. 140 141 for (SearchIndexableResource resource : resList) { 142 int xmlResId = resource.xmlResId; 143 if (xmlResId == 0) { 144 Log.e(TAG, fragmentName + " provides invalid XML (0) in search provider."); 145 continue; 146 } 147 148 List<SliceData> xmlSliceData = getSliceDataFromXML(xmlResId, fragmentName); 149 sliceData.addAll(xmlSliceData); 150 } 151 152 return sliceData; 153 } 154 getSliceDataFromXML(int xmlResId, String fragmentName)155 private List<SliceData> getSliceDataFromXML(int xmlResId, String fragmentName) { 156 XmlResourceParser parser = null; 157 158 final List<SliceData> xmlSliceData = new ArrayList<>(); 159 String controllerClassName = ""; 160 161 try { 162 parser = mContext.getResources().getXml(xmlResId); 163 164 int type; 165 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 166 && type != XmlPullParser.START_TAG) { 167 // Parse next until start tag is found 168 } 169 170 String nodeName = parser.getName(); 171 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 172 throw new RuntimeException( 173 "XML document must start with <PreferenceScreen> tag; found" 174 + nodeName + " at " + parser.getPositionDescription()); 175 } 176 177 final AttributeSet attrs = Xml.asAttributeSet(parser); 178 final String screenTitle = PreferenceXmlParserUtils.getDataTitle(mContext, attrs); 179 180 // TODO (b/67996923) Investigate if we need headers for Slices, since they never 181 // correspond to an actual setting. 182 183 final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext, 184 xmlResId, 185 MetadataFlag.FLAG_NEED_KEY 186 | MetadataFlag.FLAG_NEED_PREF_CONTROLLER 187 | MetadataFlag.FLAG_NEED_PREF_TYPE 188 | MetadataFlag.FLAG_NEED_PREF_TITLE 189 | MetadataFlag.FLAG_NEED_PREF_ICON 190 | MetadataFlag.FLAG_NEED_PREF_SUMMARY 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 BasePreferenceController controller = SliceBuilderUtils 203 .getPreferenceController(mContext, controllerClassName, key); 204 // Only add pre-approved Slices available on the device. 205 if (!controller.isSliceable() || !controller.isAvailable()) { 206 continue; 207 } 208 final String title = bundle.getString(METADATA_TITLE); 209 final String summary = bundle.getString(METADATA_SUMMARY); 210 final int iconResId = bundle.getInt(METADATA_ICON); 211 212 final int sliceType = controller.getSliceType(); 213 final String unavailableSliceSubtitle = bundle.getString( 214 METADATA_UNAVAILABLE_SLICE_SUBTITLE); 215 final boolean isPublicSlice = controller.isPublicSlice(); 216 final int highlightMenuRes = controller.getSliceHighlightMenuRes(); 217 218 final SliceData xmlSlice = new SliceData.Builder() 219 .setKey(key) 220 .setUri(controller.getSliceUri()) 221 .setTitle(title) 222 .setSummary(summary) 223 .setIcon(iconResId) 224 .setScreenTitle(screenTitle) 225 .setPreferenceControllerClassName(controllerClassName) 226 .setFragmentName(fragmentName) 227 .setSliceType(sliceType) 228 .setUnavailableSliceSubtitle(unavailableSliceSubtitle) 229 .setIsPublicSlice(isPublicSlice) 230 .setHighlightMenuRes(highlightMenuRes) 231 .build(); 232 233 xmlSliceData.add(xmlSlice); 234 } 235 } catch (SliceData.InvalidSliceDataException e) { 236 Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e); 237 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 238 SettingsEnums.ACTION_VERIFY_SLICE_ERROR_INVALID_DATA, 239 SettingsEnums.PAGE_UNKNOWN, 240 controllerClassName, 241 1); 242 } catch (XmlPullParserException | IOException | Resources.NotFoundException e) { 243 Log.w(TAG, "Error parsing PreferenceScreen: ", e); 244 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 245 SettingsEnums.ACTION_VERIFY_SLICE_PARSING_ERROR, 246 SettingsEnums.PAGE_UNKNOWN, 247 fragmentName, 248 1); 249 } catch (Exception e) { 250 Log.w(TAG, "Get slice data from XML failed ", e); 251 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 252 SettingsEnums.ACTION_VERIFY_SLICE_OTHER_EXCEPTION, 253 SettingsEnums.PAGE_UNKNOWN, 254 fragmentName + "_" + controllerClassName, 255 1); 256 } finally { 257 if (parser != null) parser.close(); 258 } 259 return xmlSliceData; 260 } 261 getAccessibilitySliceData()262 private List<SliceData> getAccessibilitySliceData() { 263 final List<SliceData> sliceData = new ArrayList<>(); 264 265 final String accessibilityControllerClassName = 266 AccessibilitySlicePreferenceController.class.getName(); 267 final String fragmentClassName = AccessibilitySettings.class.getName(); 268 final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings); 269 270 final SliceData.Builder sliceDataBuilder = new SliceData.Builder() 271 .setFragmentName(fragmentClassName) 272 .setScreenTitle(screenTitle) 273 .setPreferenceControllerClassName(accessibilityControllerClassName); 274 275 final Set<String> a11yServiceNames = new HashSet<>(); 276 Collections.addAll(a11yServiceNames, mContext.getResources() 277 .getStringArray(R.array.config_settings_slices_accessibility_components)); 278 final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList(); 279 final PackageManager packageManager = mContext.getPackageManager(); 280 281 for (AccessibilityServiceInfo a11yServiceInfo : installedServices) { 282 final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo(); 283 final ServiceInfo serviceInfo = resolveInfo.serviceInfo; 284 final String packageName = serviceInfo.packageName; 285 final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); 286 final String flattenedName = componentName.flattenToString(); 287 288 if (!a11yServiceNames.contains(flattenedName)) { 289 continue; 290 } 291 292 final String title = resolveInfo.loadLabel(packageManager).toString(); 293 int iconResource = resolveInfo.getIconResource(); 294 if (iconResource == 0) { 295 iconResource = R.drawable.ic_accessibility_generic; 296 } 297 298 sliceDataBuilder.setKey(flattenedName) 299 .setTitle(title) 300 .setUri(new Uri.Builder() 301 .scheme(ContentResolver.SCHEME_CONTENT) 302 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 303 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 304 .appendPath(flattenedName) 305 .build()) 306 .setIcon(iconResource) 307 .setSliceType(SliceData.SliceType.SWITCH); 308 try { 309 sliceData.add(sliceDataBuilder.build()); 310 } catch (SliceData.InvalidSliceDataException e) { 311 Log.w(TAG, "Invalid data when building a11y SliceData for " + flattenedName, e); 312 } 313 } 314 315 return sliceData; 316 } 317 318 @VisibleForTesting getAccessibilityServiceInfoList()319 List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() { 320 final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance( 321 mContext); 322 return accessibilityManager.getInstalledAccessibilityServiceList(); 323 } 324 }