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