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 
25 import android.annotation.XmlRes;
26 import android.content.Context;
27 import android.os.Bundle;
28 import android.provider.SearchIndexableResource;
29 import android.util.Log;
30 
31 import androidx.annotation.CallSuper;
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.settings.core.BasePreferenceController;
35 import com.android.settings.core.PreferenceControllerListHelper;
36 import com.android.settings.core.PreferenceControllerMixin;
37 import com.android.settings.core.PreferenceXmlParserUtils;
38 import com.android.settingslib.core.AbstractPreferenceController;
39 import com.android.settingslib.search.Indexable;
40 import com.android.settingslib.search.SearchIndexableRaw;
41 
42 import org.xmlpull.v1.XmlPullParserException;
43 
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 
49 /**
50  * A basic SearchIndexProvider that returns no data to index.
51  */
52 public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider {
53 
54     private static final String TAG = "BaseSearchIndex";
55     private int mXmlRes = 0;
56 
BaseSearchIndexProvider()57     public BaseSearchIndexProvider() {
58     }
59 
BaseSearchIndexProvider(int xmlRes)60     public BaseSearchIndexProvider(int xmlRes) {
61         mXmlRes = xmlRes;
62     }
63 
64     @Override
getXmlResourcesToIndex(Context context, boolean enabled)65     public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) {
66         if (mXmlRes != 0) {
67             final SearchIndexableResource sir = new SearchIndexableResource(context);
68             sir.xmlResId = mXmlRes;
69             return Arrays.asList(sir);
70         }
71         return null;
72     }
73 
74     @Override
getRawDataToIndex(Context context, boolean enabled)75     public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
76         return null;
77     }
78 
79     @Override
80     @CallSuper
getDynamicRawDataToIndex(Context context, boolean enabled)81     public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) {
82         final List<SearchIndexableRaw> dynamicRaws = new ArrayList<>();
83         if (!isPageSearchEnabled(context)) {
84             // Entire page should be suppressed, do not add dynamic raw data.
85             return dynamicRaws;
86         }
87         final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
88         if (controllers == null || controllers.isEmpty()) {
89             return dynamicRaws;
90         }
91         for (AbstractPreferenceController controller : controllers) {
92             if (controller instanceof PreferenceControllerMixin) {
93                 ((PreferenceControllerMixin) controller).updateDynamicRawDataToIndex(dynamicRaws);
94             } else if (controller instanceof BasePreferenceController) {
95                 ((BasePreferenceController) controller).updateDynamicRawDataToIndex(dynamicRaws);
96             } else {
97                 Log.e(TAG, controller.getClass().getName()
98                         + " must implement " + PreferenceControllerMixin.class.getName()
99                         + " treating the dynamic indexable");
100             }
101         }
102         return dynamicRaws;
103     }
104 
105     @Override
106     @CallSuper
getNonIndexableKeys(Context context)107     public List<String> getNonIndexableKeys(Context context) {
108         if (!isPageSearchEnabled(context)) {
109             // Entire page should be suppressed, mark all keys from this page as non-indexable.
110             return getNonIndexableKeysFromXml(context, true /* suppressAllPage */);
111         }
112         final List<String> nonIndexableKeys = new ArrayList<>();
113         nonIndexableKeys.addAll(getNonIndexableKeysFromXml(context, false /* suppressAllPage */));
114         final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
115         if (controllers != null && !controllers.isEmpty()) {
116             for (AbstractPreferenceController controller : controllers) {
117                 if (controller instanceof PreferenceControllerMixin) {
118                     ((PreferenceControllerMixin) controller)
119                             .updateNonIndexableKeys(nonIndexableKeys);
120                 } else if (controller instanceof BasePreferenceController) {
121                     ((BasePreferenceController) controller).updateNonIndexableKeys(
122                             nonIndexableKeys);
123                 } else {
124                     Log.e(TAG, controller.getClass().getName()
125                             + " must implement " + PreferenceControllerMixin.class.getName()
126                             + " treating the key non-indexable");
127                     nonIndexableKeys.add(controller.getPreferenceKey());
128                 }
129             }
130         }
131         return nonIndexableKeys;
132     }
133 
getPreferenceControllers(Context context)134     public List<AbstractPreferenceController> getPreferenceControllers(Context context) {
135         List<AbstractPreferenceController> controllersFromCode = new ArrayList<>();
136         try {
137             controllersFromCode = createPreferenceControllers(context);
138         } catch (Exception e) {
139             Log.w(TAG, "Error initial controller");
140         }
141 
142         final List<SearchIndexableResource> res = getXmlResourcesToIndex(context, true);
143         if (res == null || res.isEmpty()) {
144             return controllersFromCode;
145         }
146         List<BasePreferenceController> controllersFromXml = new ArrayList<>();
147         for (SearchIndexableResource sir : res) {
148             controllersFromXml.addAll(PreferenceControllerListHelper
149                     .getPreferenceControllersFromXml(context, sir.xmlResId));
150         }
151         controllersFromXml = PreferenceControllerListHelper.filterControllers(controllersFromXml,
152                 controllersFromCode);
153         final List<AbstractPreferenceController> allControllers = new ArrayList<>();
154         if (controllersFromCode != null) {
155             allControllers.addAll(controllersFromCode);
156         }
157         allControllers.addAll(controllersFromXml);
158         return allControllers;
159     }
160 
161     /**
162      * Creates a list of {@link AbstractPreferenceController} programatically.
163      * <p/>
164      * This list should create controllers that are not defined in xml as a Slice controller.
165      */
createPreferenceControllers(Context context)166     public List<AbstractPreferenceController> createPreferenceControllers(Context context) {
167         return null;
168     }
169 
170     /**
171      * Returns true if the page should be considered in search query. If return false, entire page
172      * will be suppressed during search query.
173      */
isPageSearchEnabled(Context context)174     protected boolean isPageSearchEnabled(Context context) {
175         return true;
176     }
177 
178     /**
179      * Get all non-indexable keys from xml. If {@param suppressAllPage} is set, all keys are
180      * considered non-indexable. Otherwise, only keys with searchable="false" are included.
181      */
getNonIndexableKeysFromXml(Context context, boolean suppressAllPage)182     private List<String> getNonIndexableKeysFromXml(Context context, boolean suppressAllPage) {
183         final List<SearchIndexableResource> resources = getXmlResourcesToIndex(
184                 context, true /* not used*/);
185         if (resources == null || resources.isEmpty()) {
186             return new ArrayList<>();
187         }
188         final List<String> nonIndexableKeys = new ArrayList<>();
189         for (SearchIndexableResource res : resources) {
190             nonIndexableKeys.addAll(
191                     getNonIndexableKeysFromXml(context, res.xmlResId, suppressAllPage));
192         }
193         return nonIndexableKeys;
194     }
195 
196     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId, boolean suppressAllPage)197     public List<String> getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId,
198             boolean suppressAllPage) {
199         return getKeysFromXml(context, xmlResId, suppressAllPage);
200     }
201 
getKeysFromXml(Context context, @XmlRes int xmlResId, boolean suppressAllPage)202     private List<String> getKeysFromXml(Context context, @XmlRes int xmlResId,
203             boolean suppressAllPage) {
204         final List<String> keys = new ArrayList<>();
205         try {
206             final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(context,
207                     xmlResId, FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN | FLAG_NEED_SEARCHABLE);
208             for (Bundle bundle : metadata) {
209                 if (suppressAllPage || !bundle.getBoolean(METADATA_SEARCHABLE, true)) {
210                     keys.add(bundle.getString(METADATA_KEY));
211                 }
212             }
213         } catch (IOException | XmlPullParserException e) {
214             Log.w(TAG, "Error parsing non-indexable from xml " + xmlResId);
215         }
216         return keys;
217     }
218 }
219