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