• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SliceDataConverter(Context context)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      */
getSliceData()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 
getSliceDataFromProvider(SearchIndexProvider provider, String fragmentName)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 
getSliceDataFromXML(int xmlResId, String fragmentName)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 
getAccessibilitySliceData()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
getAccessibilityServiceInfoList()291     List<AccessibilityServiceInfo> getAccessibilityServiceInfoList() {
292         final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(
293                 mContext);
294         return accessibilityManager.getInstalledAccessibilityServiceList();
295     }
296 }