1/* 2 * Copyright (C) 2016 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 17package com.android.settings.search; 18 19import static com.google.common.truth.Truth.assertWithMessage; 20 21import android.provider.SearchIndexableResource; 22import android.util.ArraySet; 23import android.util.Log; 24 25import com.android.settings.SettingsPreferenceFragment; 26import com.android.settings.core.codeinspection.CodeInspector; 27import com.android.settings.dashboard.DashboardFragmentSearchIndexProviderInspector; 28 29import org.robolectric.RuntimeEnvironment; 30 31import java.lang.reflect.Field; 32import java.util.ArrayList; 33import java.util.List; 34import java.util.Set; 35 36/** 37 * {@link CodeInspector} to ensure fragments implement search components correctly. 38 */ 39public class SearchIndexProviderCodeInspector extends CodeInspector { 40 private static final String TAG = "SearchCodeInspector"; 41 42 private static final String NOT_IMPLEMENTING_INDEXABLE_ERROR = 43 "SettingsPreferenceFragment should implement Indexable, but these do not:\n"; 44 private static final String NOT_CONTAINING_PROVIDER_OBJECT_ERROR = 45 "Indexable should have public field " 46 + DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER 47 + " but these are not:\n"; 48 private static final String NOT_SHARING_PREF_CONTROLLERS_BETWEEN_FRAG_AND_PROVIDER = 49 "DashboardFragment should share pref controllers with its SearchIndexProvider, but " 50 + " these are not: \n"; 51 private static final String NOT_IN_INDEXABLE_PROVIDER_REGISTRY = 52 "Class containing " + DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER 53 + " must be added to " + SearchIndexableResources.class.getName() 54 + " but these are not: \n"; 55 private static final String NOT_PROVIDING_VALID_RESOURCE_ERROR = 56 "SearchIndexableProvider must either provide no resource to index, or valid ones. " 57 + "But the followings contain resource with xml id = 0\n"; 58 59 private final List<String> notImplementingIndexableGrandfatherList; 60 private final List<String> notImplementingIndexProviderGrandfatherList; 61 private final List<String> notInSearchIndexableRegistryGrandfatherList; 62 private final List<String> notSharingPrefControllersGrandfatherList; 63 64 public SearchIndexProviderCodeInspector(List<Class<?>> classes) { 65 super(classes); 66 notImplementingIndexableGrandfatherList = new ArrayList<>(); 67 notImplementingIndexProviderGrandfatherList = new ArrayList<>(); 68 notInSearchIndexableRegistryGrandfatherList = new ArrayList<>(); 69 notSharingPrefControllersGrandfatherList = new ArrayList<>(); 70 initializeGrandfatherList(notImplementingIndexableGrandfatherList, 71 "grandfather_not_implementing_indexable"); 72 initializeGrandfatherList(notImplementingIndexProviderGrandfatherList, 73 "grandfather_not_implementing_index_provider"); 74 initializeGrandfatherList(notInSearchIndexableRegistryGrandfatherList, 75 "grandfather_not_in_search_index_provider_registry"); 76 initializeGrandfatherList(notSharingPrefControllersGrandfatherList, 77 "grandfather_not_sharing_pref_controllers_with_search_provider"); 78 } 79 80 @Override 81 public void run() { 82 final Set<String> notImplementingIndexable = new ArraySet<>(); 83 final Set<String> notImplementingIndexProvider = new ArraySet<>(); 84 final Set<String> notInSearchProviderRegistry = new ArraySet<>(); 85 final Set<String> notSharingPreferenceControllers = new ArraySet<>(); 86 final Set<String> notProvidingValidResource = new ArraySet<>(); 87 88 for (Class clazz : mClasses) { 89 if (!isConcreteSettingsClass(clazz)) { 90 continue; 91 } 92 final String className = clazz.getName(); 93 // Skip fragments if it's not SettingsPreferenceFragment. 94 if (!SettingsPreferenceFragment.class.isAssignableFrom(clazz)) { 95 continue; 96 } 97 // If it's a SettingsPreferenceFragment, it must also be Indexable. 98 final boolean implementsIndexable = Indexable.class.isAssignableFrom(clazz); 99 if (!implementsIndexable) { 100 if (!notImplementingIndexableGrandfatherList.remove(className)) { 101 notImplementingIndexable.add(className); 102 } 103 continue; 104 } 105 final boolean hasSearchIndexProvider = hasSearchIndexProvider(clazz); 106 // If it implements Indexable, it must also implement the index provider field. 107 if (!hasSearchIndexProvider) { 108 if (!notImplementingIndexProviderGrandfatherList.remove(className)) { 109 notImplementingIndexProvider.add(className); 110 } 111 continue; 112 } 113 // If it implements index provider field AND it's a DashboardFragment, its fragment and 114 // search provider must share the same set of PreferenceControllers. 115 final boolean isSharingPrefControllers = DashboardFragmentSearchIndexProviderInspector 116 .isSharingPreferenceControllers(clazz); 117 if (!isSharingPrefControllers) { 118 if (!notSharingPrefControllersGrandfatherList.remove(className)) { 119 notSharingPreferenceControllers.add(className); 120 } 121 continue; 122 } 123 // Must be in SearchProviderRegistry 124 SearchFeatureProvider provider = new SearchFeatureProviderImpl(); 125 if (!provider.getSearchIndexableResources().getProviderValues().contains(clazz)) { 126 if (!notInSearchIndexableRegistryGrandfatherList.remove(className)) { 127 notInSearchProviderRegistry.add(className); 128 } 129 } 130 // Search provider must either don't provider resource xml, or provide valid ones. 131 if (!hasValidResourceFromProvider(clazz)) { 132 notProvidingValidResource.add(className); 133 } 134 } 135 136 // Build error messages 137 final String indexableError = buildErrorMessage(NOT_IMPLEMENTING_INDEXABLE_ERROR, 138 notImplementingIndexable); 139 final String indexProviderError = buildErrorMessage(NOT_CONTAINING_PROVIDER_OBJECT_ERROR, 140 notImplementingIndexProvider); 141 final String notSharingPrefControllerError = buildErrorMessage( 142 NOT_SHARING_PREF_CONTROLLERS_BETWEEN_FRAG_AND_PROVIDER, 143 notSharingPreferenceControllers); 144 final String notInProviderRegistryError = 145 buildErrorMessage(NOT_IN_INDEXABLE_PROVIDER_REGISTRY, notInSearchProviderRegistry); 146 final String notProvidingValidResourceError = buildErrorMessage( 147 NOT_PROVIDING_VALID_RESOURCE_ERROR, notProvidingValidResource); 148 assertWithMessage(indexableError) 149 .that(notImplementingIndexable) 150 .isEmpty(); 151 assertWithMessage(indexProviderError) 152 .that(notImplementingIndexProvider) 153 .isEmpty(); 154 assertWithMessage(notSharingPrefControllerError) 155 .that(notSharingPreferenceControllers) 156 .isEmpty(); 157 assertWithMessage(notInProviderRegistryError) 158 .that(notInSearchProviderRegistry) 159 .isEmpty(); 160 assertWithMessage(notProvidingValidResourceError) 161 .that(notProvidingValidResource) 162 .isEmpty(); 163 assertNoObsoleteInGrandfatherList("grandfather_not_implementing_indexable", 164 notImplementingIndexableGrandfatherList); 165 assertNoObsoleteInGrandfatherList("grandfather_not_implementing_index_provider", 166 notImplementingIndexProviderGrandfatherList); 167 assertNoObsoleteInGrandfatherList("grandfather_not_in_search_index_provider_registry", 168 notInSearchIndexableRegistryGrandfatherList); 169 assertNoObsoleteInGrandfatherList( 170 "grandfather_not_sharing_pref_controllers_with_search_provider", 171 notSharingPrefControllersGrandfatherList); 172 } 173 174 private boolean hasSearchIndexProvider(Class clazz) { 175 try { 176 final Field f = clazz.getField( 177 DatabaseIndexingUtils.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); 178 return f != null; 179 } catch (NoClassDefFoundError e) { 180 // Cannot find class def, ignore 181 return true; 182 } catch (NoSuchFieldException e) { 183 Log.e(TAG, "error fetching search provider from class " + clazz.getName()); 184 return false; 185 } 186 } 187 188 private boolean hasValidResourceFromProvider(Class clazz) { 189 try { 190 final Indexable.SearchIndexProvider provider = 191 DatabaseIndexingUtils.getSearchIndexProvider(clazz); 192 final List<SearchIndexableResource> resources = provider.getXmlResourcesToIndex( 193 RuntimeEnvironment.application, true /* enabled */); 194 if (resources == null) { 195 // No resource, that's fine. 196 return true; 197 } 198 for (SearchIndexableResource res : resources) { 199 if (res.xmlResId == 0) { 200 // Invalid resource 201 return false; 202 } 203 } 204 } catch (Exception e) { 205 // Ignore. 206 } 207 return true; 208 } 209 210 private String buildErrorMessage(String errorSummary, Set<String> errorClasses) { 211 final StringBuilder error = new StringBuilder(errorSummary); 212 for (String c : errorClasses) { 213 error.append(c).append("\n"); 214 } 215 return error.toString(); 216 } 217} 218