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