• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.search;
18 
19 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY;
20 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SEARCHABLE;
21 import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_INCLUDE_PREF_SCREEN;
22 import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_KEY;
23 import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_SEARCHABLE;
24 import static com.android.settings.search.SettingsSearchIndexablesProvider.SYSPROP_CRASH_ON_ERROR;
25 
26 import android.annotation.XmlRes;
27 import android.content.Context;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.provider.SearchIndexableResource;
31 import android.util.Log;
32 
33 import androidx.annotation.CallSuper;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.settings.core.BasePreferenceController;
37 import com.android.settings.core.PreferenceControllerListHelper;
38 import com.android.settings.core.PreferenceControllerMixin;
39 import com.android.settings.core.PreferenceXmlParserUtils;
40 import com.android.settingslib.core.AbstractPreferenceController;
41 import com.android.settingslib.search.Indexable;
42 import com.android.settingslib.search.SearchIndexableRaw;
43 
44 import org.xmlpull.v1.XmlPullParserException;
45 
46 import java.io.IOException;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.List;
50 import java.util.stream.Collectors;
51 
52 /**
53  * A basic SearchIndexProvider that returns no data to index.
54  */
55 public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider {
56 
57     private static final String TAG = "BaseSearchIndex";
58     private int mXmlRes = 0;
59 
BaseSearchIndexProvider()60     public BaseSearchIndexProvider() {
61     }
62 
BaseSearchIndexProvider(int xmlRes)63     public BaseSearchIndexProvider(int xmlRes) {
64         mXmlRes = xmlRes;
65     }
66 
67     @Override
getXmlResourcesToIndex(Context context, boolean enabled)68     public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) {
69         if (mXmlRes != 0) {
70             final SearchIndexableResource sir = new SearchIndexableResource(context);
71             sir.xmlResId = mXmlRes;
72             return Arrays.asList(sir);
73         }
74         return null;
75     }
76 
77     @Override
getRawDataToIndex(Context context, boolean enabled)78     public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
79         final List<SearchIndexableRaw> raws = new ArrayList<>();
80         final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
81         if (controllers == null || controllers.isEmpty()) {
82             return raws;
83         }
84         for (AbstractPreferenceController controller : controllers) {
85             if (controller instanceof PreferenceControllerMixin) {
86                 ((PreferenceControllerMixin) controller).updateRawDataToIndex(raws);
87             } else if (controller instanceof BasePreferenceController) {
88                 ((BasePreferenceController) controller).updateRawDataToIndex(raws);
89             }
90         }
91         return raws;
92     }
93 
94     @Override
95     @CallSuper
getDynamicRawDataToIndex(Context context, boolean enabled)96     public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) {
97         final List<SearchIndexableRaw> dynamicRaws = new ArrayList<>();
98         if (!isPageSearchEnabled(context)) {
99             // Entire page should be suppressed, do not add dynamic raw data.
100             return dynamicRaws;
101         }
102         final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
103         if (controllers == null || controllers.isEmpty()) {
104             return dynamicRaws;
105         }
106         for (AbstractPreferenceController controller : controllers) {
107             if (controller instanceof PreferenceControllerMixin) {
108                 ((PreferenceControllerMixin) controller).updateDynamicRawDataToIndex(dynamicRaws);
109             } else if (controller instanceof BasePreferenceController) {
110                 ((BasePreferenceController) controller).updateDynamicRawDataToIndex(dynamicRaws);
111             } else {
112                 Log.e(TAG, controller.getClass().getName()
113                         + " must implement " + PreferenceControllerMixin.class.getName()
114                         + " treating the dynamic indexable");
115             }
116         }
117         return dynamicRaws;
118     }
119 
120     @Override
121     @CallSuper
getNonIndexableKeys(Context context)122     public List<String> getNonIndexableKeys(Context context) {
123         final List<String> nonIndexableKeys = new ArrayList<>();
124         if (!isPageSearchEnabled(context)) {
125             // Entire page should be suppressed, mark all keys from this page as non-indexable.
126             nonIndexableKeys.addAll(
127                     getNonIndexableKeysFromXml(context, true /* suppressAllPage */));
128             nonIndexableKeys.addAll(
129                     getRawDataToIndex(context, true /* enabled */)
130                             .stream()
131                             .map(data -> data.key)
132                             .collect(Collectors.toList()));
133             return nonIndexableKeys;
134         }
135         nonIndexableKeys.addAll(getNonIndexableKeysFromXml(context, false /* suppressAllPage */));
136         updateNonIndexableKeysFromControllers(context, nonIndexableKeys);
137         return nonIndexableKeys;
138     }
139 
updateNonIndexableKeysFromControllers( Context context, List<String> nonIndexableKeys)140     private void updateNonIndexableKeysFromControllers(
141             Context context, List<String> nonIndexableKeys) {
142         final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
143         if (controllers != null) {
144             for (AbstractPreferenceController controller : controllers) {
145                 updateNonIndexableKeysFromController(nonIndexableKeys, controller);
146             }
147         }
148     }
149 
updateNonIndexableKeysFromController( List<String> nonIndexableKeys, AbstractPreferenceController controller)150     private static void updateNonIndexableKeysFromController(
151             List<String> nonIndexableKeys, AbstractPreferenceController controller) {
152         try {
153             if (controller instanceof PreferenceControllerMixin controllerMixin) {
154                 controllerMixin.updateNonIndexableKeys(nonIndexableKeys);
155             } else if (controller instanceof BasePreferenceController basePreferenceController) {
156                 basePreferenceController.updateNonIndexableKeys(nonIndexableKeys);
157             } else {
158                 Log.e(TAG, controller.getClass().getName()
159                         + " must implement " + PreferenceControllerMixin.class.getName()
160                         + " treating the key non-indexable");
161                 nonIndexableKeys.add(controller.getPreferenceKey());
162             }
163         } catch (Exception e) {
164             String msg = "Error trying to get non-indexable keys from: " + controller;
165             // Catch a generic crash. In the absence of the catch, the background thread will
166             // silently fail anyway, so we aren't losing information by catching the exception.
167             // We crash on debuggable build or when the system property exists, so that we can test
168             // if crashes need to be fixed.
169             // The gain is that if there is a crash in a specific controller, we don't lose all
170             // non-indexable keys, but we can still find specific crashes in development.
171             if (Build.IS_DEBUGGABLE || System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
172                 throw new RuntimeException(msg, e);
173             }
174             Log.e(TAG, msg, e);
175             // When there is an error, treat the key as non-indexable.
176             nonIndexableKeys.add(controller.getPreferenceKey());
177         }
178     }
179 
getPreferenceControllers(Context context)180     public List<AbstractPreferenceController> getPreferenceControllers(Context context) {
181         List<AbstractPreferenceController> controllersFromCode = new ArrayList<>();
182         try {
183             controllersFromCode = createPreferenceControllers(context);
184         } catch (Exception e) {
185             Log.w(TAG, "Error initializing controller in fragment: " + this + ", e: " + e);
186         }
187 
188         final List<SearchIndexableResource> res = getXmlResourcesToIndex(context, true);
189         if (res == null || res.isEmpty()) {
190             return controllersFromCode;
191         }
192         List<BasePreferenceController> controllersFromXml = new ArrayList<>();
193         for (SearchIndexableResource sir : res) {
194             controllersFromXml.addAll(PreferenceControllerListHelper
195                     .getPreferenceControllersFromXml(context, sir.xmlResId));
196         }
197         controllersFromXml = PreferenceControllerListHelper.filterControllers(controllersFromXml,
198                 controllersFromCode);
199         final List<AbstractPreferenceController> allControllers = new ArrayList<>();
200         if (controllersFromCode != null) {
201             allControllers.addAll(controllersFromCode);
202         }
203         allControllers.addAll(controllersFromXml);
204         return allControllers;
205     }
206 
207     /**
208      * Creates a list of {@link AbstractPreferenceController} programatically.
209      * <p/>
210      * This list should create controllers that are not defined in xml as a Slice controller.
211      */
createPreferenceControllers(Context context)212     public List<AbstractPreferenceController> createPreferenceControllers(Context context) {
213         return null;
214     }
215 
216     /**
217      * Returns true if the page should be considered in search query. If return false, entire page
218      * will be suppressed during search query.
219      */
isPageSearchEnabled(Context context)220     protected boolean isPageSearchEnabled(Context context) {
221         return true;
222     }
223 
224     /**
225      * Get all non-indexable keys from xml. If {@param suppressAllPage} is set, all keys are
226      * considered non-indexable. Otherwise, only keys with searchable="false" are included.
227      */
getNonIndexableKeysFromXml(Context context, boolean suppressAllPage)228     private List<String> getNonIndexableKeysFromXml(Context context, boolean suppressAllPage) {
229         final List<SearchIndexableResource> resources = getXmlResourcesToIndex(
230                 context, true /* not used*/);
231         if (resources == null || resources.isEmpty()) {
232             return new ArrayList<>();
233         }
234         final List<String> nonIndexableKeys = new ArrayList<>();
235         for (SearchIndexableResource res : resources) {
236             nonIndexableKeys.addAll(
237                     getNonIndexableKeysFromXml(context, res.xmlResId, suppressAllPage));
238         }
239         return nonIndexableKeys;
240     }
241 
242     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId, boolean suppressAllPage)243     public List<String> getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId,
244             boolean suppressAllPage) {
245         return getKeysFromXml(context, xmlResId, suppressAllPage);
246     }
247 
getKeysFromXml(Context context, @XmlRes int xmlResId, boolean suppressAllPage)248     private List<String> getKeysFromXml(Context context, @XmlRes int xmlResId,
249             boolean suppressAllPage) {
250         final List<String> keys = new ArrayList<>();
251         try {
252             final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(context,
253                     xmlResId, FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN | FLAG_NEED_SEARCHABLE);
254             for (Bundle bundle : metadata) {
255                 if (suppressAllPage || !bundle.getBoolean(METADATA_SEARCHABLE, true)) {
256                     keys.add(bundle.getString(METADATA_KEY));
257                 }
258             }
259         } catch (IOException | XmlPullParserException e) {
260             Log.w(TAG, "Error parsing non-indexable from xml " + xmlResId);
261         }
262         return keys;
263     }
264 }
265