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_PREF_TYPE; 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 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_USER_RESTRICTION; 27 import static com.android.settings.core.PreferenceXmlParserUtils.PREF_SCREEN_TAG; 28 29 import android.accessibilityservice.AccessibilityServiceInfo; 30 import android.app.settings.SettingsEnums; 31 import android.content.ComponentName; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ResolveInfo; 36 import android.content.pm.ServiceInfo; 37 import android.content.res.Resources; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.provider.SearchIndexableResource; 41 import android.provider.SettingsSlicesContract; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.view.accessibility.AccessibilityManager; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.settings.R; 50 import com.android.settings.accessibility.AccessibilitySettings; 51 import com.android.settings.accessibility.AccessibilitySlicePreferenceController; 52 import com.android.settings.core.BasePreferenceController; 53 import com.android.settings.core.PreferenceXmlParserUtils; 54 import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; 55 import com.android.settings.dashboard.DashboardFragment; 56 import com.android.settings.notification.RingerModeAffectedVolumePreferenceController; 57 import com.android.settings.overlay.FeatureFactory; 58 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 59 import com.android.settingslib.search.Indexable.SearchIndexProvider; 60 import com.android.settingslib.search.SearchIndexableData; 61 62 import org.xmlpull.v1.XmlPullParserException; 63 64 import java.io.IOException; 65 import java.util.ArrayList; 66 import java.util.Collection; 67 import java.util.Collections; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Set; 71 72 /** 73 * Converts all Slice sources into {@link SliceData}. 74 * This includes: 75 * - All {@link DashboardFragment DashboardFragments} indexed by settings search 76 * - Accessibility services 77 */ 78 class SliceDataConverter { 79 80 private static final String TAG = "SliceDataConverter"; 81 82 private final MetricsFeatureProvider mMetricsFeatureProvider; 83 private Context mContext; 84 SliceDataConverter(Context context)85 public SliceDataConverter(Context context) { 86 mContext = context; 87 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 88 } 89 90 /** 91 * @return a list of {@link SliceData} to be indexed and later referenced as a Slice. 92 * 93 * The collection works as follows: 94 * - Collects a list of Fragments from 95 * {@link FeatureFactory#getSearchFeatureProvider()}. 96 * - From each fragment, grab a {@link SearchIndexProvider}. 97 * - For each provider, collect XML resource layout and a list of 98 * {@link com.android.settings.core.BasePreferenceController}. 99 */ getSliceData()100 public List<SliceData> getSliceData() { 101 List<SliceData> sliceData = new ArrayList<>(); 102 103 final Collection<SearchIndexableData> bundles = FeatureFactory.getFeatureFactory() 104 .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues(); 105 106 for (SearchIndexableData bundle : bundles) { 107 final String fragmentName = bundle.getTargetClass().getName(); 108 109 final SearchIndexProvider provider = bundle.getSearchIndexProvider(); 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 final List<SliceData> xmlSliceData = new ArrayList<>(); 156 String controllerClassName = ""; 157 @NonNull String screenTitle = ""; 158 159 try { 160 // TODO (b/67996923) Investigate if we need headers for Slices, since they never 161 // correspond to an actual setting. 162 163 final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext, 164 xmlResId, 165 MetadataFlag.FLAG_INCLUDE_PREF_SCREEN 166 | MetadataFlag.FLAG_NEED_KEY 167 | MetadataFlag.FLAG_NEED_PREF_CONTROLLER 168 | MetadataFlag.FLAG_NEED_PREF_TYPE 169 | MetadataFlag.FLAG_NEED_PREF_TITLE 170 | MetadataFlag.FLAG_NEED_PREF_ICON 171 | MetadataFlag.FLAG_NEED_PREF_SUMMARY 172 | MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE 173 | MetadataFlag.FLAG_NEED_USER_RESTRICTION); 174 175 for (Bundle bundle : metadata) { 176 final String title = bundle.getString(METADATA_TITLE); 177 if (PREF_SCREEN_TAG.equals(bundle.getString(METADATA_PREF_TYPE))) { 178 if (title != null) { 179 screenTitle = title; 180 } 181 continue; 182 } 183 // TODO (b/67996923) Non-controller Slices should become intent-only slices. 184 // Note that without a controller, dynamic summaries are impossible. 185 controllerClassName = bundle.getString(METADATA_CONTROLLER); 186 if (TextUtils.isEmpty(controllerClassName)) { 187 continue; 188 } 189 190 final String key = bundle.getString(METADATA_KEY); 191 final BasePreferenceController controller = SliceBuilderUtils 192 .getPreferenceController(mContext, controllerClassName, key); 193 // Only add pre-approved Slices available on the device. 194 // Always index RingerModeAffected slices so they are available for panel 195 if (!controller.isSliceable() 196 || !(controller.isAvailable() 197 || controller instanceof RingerModeAffectedVolumePreferenceController)) { 198 continue; 199 } 200 final String summary = bundle.getString(METADATA_SUMMARY); 201 final int iconResId = bundle.getInt(METADATA_ICON); 202 203 final int sliceType = controller.getSliceType(); 204 final String unavailableSliceSubtitle = bundle.getString( 205 METADATA_UNAVAILABLE_SLICE_SUBTITLE); 206 final boolean isPublicSlice = controller.isPublicSlice(); 207 final int highlightMenuRes = controller.getSliceHighlightMenuRes(); 208 final String userRestriction = bundle.getString(METADATA_USER_RESTRICTION); 209 210 final SliceData xmlSlice = new SliceData.Builder() 211 .setKey(key) 212 .setUri(controller.getSliceUri()) 213 .setTitle(title) 214 .setSummary(summary) 215 .setIcon(iconResId) 216 .setScreenTitle(screenTitle) 217 .setPreferenceControllerClassName(controllerClassName) 218 .setFragmentName(fragmentName) 219 .setSliceType(sliceType) 220 .setUnavailableSliceSubtitle(unavailableSliceSubtitle) 221 .setIsPublicSlice(isPublicSlice) 222 .setHighlightMenuRes(highlightMenuRes) 223 .setUserRestriction(userRestriction) 224 .build(); 225 226 xmlSliceData.add(xmlSlice); 227 } 228 } catch (SliceData.InvalidSliceDataException e) { 229 Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e); 230 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 231 SettingsEnums.ACTION_VERIFY_SLICE_ERROR_INVALID_DATA, 232 SettingsEnums.PAGE_UNKNOWN, 233 controllerClassName, 234 1); 235 } catch (XmlPullParserException | IOException | Resources.NotFoundException e) { 236 Log.w(TAG, "Error parsing PreferenceScreen: ", e); 237 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 238 SettingsEnums.ACTION_VERIFY_SLICE_PARSING_ERROR, 239 SettingsEnums.PAGE_UNKNOWN, 240 fragmentName, 241 1); 242 } catch (Exception e) { 243 Log.w(TAG, "Get slice data from XML failed ", e); 244 mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, 245 SettingsEnums.ACTION_VERIFY_SLICE_OTHER_EXCEPTION, 246 SettingsEnums.PAGE_UNKNOWN, 247 fragmentName + "_" + controllerClassName, 248 1); 249 } 250 return xmlSliceData; 251 } 252 getAccessibilitySliceData()253 private List<SliceData> getAccessibilitySliceData() { 254 final List<SliceData> sliceData = new ArrayList<>(); 255 256 final String accessibilityControllerClassName = 257 AccessibilitySlicePreferenceController.class.getName(); 258 final String fragmentClassName = AccessibilitySettings.class.getName(); 259 final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings); 260 261 final SliceData.Builder sliceDataBuilder = new SliceData.Builder() 262 .setFragmentName(fragmentClassName) 263 .setScreenTitle(screenTitle) 264 .setPreferenceControllerClassName(accessibilityControllerClassName); 265 266 final Set<String> a11yServiceNames = new HashSet<>(); 267 Collections.addAll(a11yServiceNames, mContext.getResources() 268 .getStringArray(R.array.config_settings_slices_accessibility_components)); 269 final List<AccessibilityServiceInfo> installedServices = getAccessibilityServiceInfoList(); 270 final PackageManager packageManager = mContext.getPackageManager(); 271 272 for (AccessibilityServiceInfo a11yServiceInfo : installedServices) { 273 final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo(); 274 final ServiceInfo serviceInfo = resolveInfo.serviceInfo; 275 final String packageName = serviceInfo.packageName; 276 final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); 277 final String flattenedName = componentName.flattenToString(); 278 279 if (!a11yServiceNames.contains(flattenedName)) { 280 continue; 281 } 282 283 final String title = resolveInfo.loadLabel(packageManager).toString(); 284 int iconResource = resolveInfo.getIconResource(); 285 if (iconResource == 0) { 286 iconResource = R.drawable.ic_accessibility_generic; 287 } 288 289 sliceDataBuilder.setKey(flattenedName) 290 .setTitle(title) 291 .setUri(new Uri.Builder() 292 .scheme(ContentResolver.SCHEME_CONTENT) 293 .authority(SettingsSliceProvider.SLICE_AUTHORITY) 294 .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) 295 .appendPath(flattenedName) 296 .build()) 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 }