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 }