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 }