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 }