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 android.util.ArraySet; 20import android.util.Log; 21 22import com.android.settings.SettingsPreferenceFragment; 23import com.android.settings.core.codeinspection.CodeInspector; 24import com.android.settings.dashboard.DashboardFragmentSearchIndexProviderInspector; 25 26import java.lang.reflect.Field; 27import java.util.ArrayList; 28import java.util.List; 29import java.util.Set; 30 31import static com.google.common.truth.Truth.assertWithMessage; 32 33/** 34 * {@link CodeInspector} to ensure fragments implement search components correctly. 35 */ 36public class SearchIndexProviderCodeInspector extends CodeInspector { 37 private static final String TAG = "SearchCodeInspector"; 38 39 private static final String NOT_IMPLEMENTING_INDEXABLE_ERROR = 40 "SettingsPreferenceFragment should implement Indexable, but these do not:\n"; 41 private static final String NOT_CONTAINING_PROVIDER_OBJECT_ERROR = 42 "Indexable should have public field " 43 + DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER 44 + " but these are not:\n"; 45 private static final String NOT_SHARING_PREF_CONTROLLERS_BETWEEN_FRAG_AND_PROVIDER = 46 "DashboardFragment should share pref controllers with its SearchIndexProvider, but " 47 + " these are not: \n"; 48 private static final String NOT_IN_INDEXABLE_PROVIDER_REGISTRY = 49 "Class containing " + DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER 50 + " must be added to " + SearchIndexableResources.class.getName() 51 + " but these are not: \n"; 52 53 private final List<String> notImplementingIndexableGrandfatherList; 54 private final List<String> notImplementingIndexProviderGrandfatherList; 55 private final List<String> notInSearchIndexableRegistryGrandfatherList; 56 private final List<String> notSharingPrefControllersGrandfatherList; 57 58 public SearchIndexProviderCodeInspector(List<Class<?>> classes) { 59 super(classes); 60 notImplementingIndexableGrandfatherList = new ArrayList<>(); 61 notImplementingIndexProviderGrandfatherList = new ArrayList<>(); 62 notInSearchIndexableRegistryGrandfatherList = new ArrayList<>(); 63 notSharingPrefControllersGrandfatherList = new ArrayList<>(); 64 initializeGrandfatherList(notImplementingIndexableGrandfatherList, 65 "grandfather_not_implementing_indexable"); 66 initializeGrandfatherList(notImplementingIndexProviderGrandfatherList, 67 "grandfather_not_implementing_index_provider"); 68 initializeGrandfatherList(notInSearchIndexableRegistryGrandfatherList, 69 "grandfather_not_in_search_index_provider_registry"); 70 initializeGrandfatherList(notSharingPrefControllersGrandfatherList, 71 "grandfather_not_sharing_pref_controllers_with_search_provider"); 72 } 73 74 @Override 75 public void run() { 76 final Set<String> notImplementingIndexable = new ArraySet<>(); 77 final Set<String> notImplementingIndexProvider = new ArraySet<>(); 78 final Set<String> notInSearchProviderRegistry = new ArraySet<>(); 79 final Set<String> notSharingPreferenceControllers = new ArraySet<>(); 80 81 for (Class clazz : mClasses) { 82 if (!isConcreteSettingsClass(clazz)) { 83 continue; 84 } 85 final String className = clazz.getName(); 86 // Skip fragments if it's not SettingsPreferenceFragment. 87 if (!SettingsPreferenceFragment.class.isAssignableFrom(clazz)) { 88 continue; 89 } 90 // If it's a SettingsPreferenceFragment, it must also be Indexable. 91 final boolean implementsIndexable = Indexable.class.isAssignableFrom(clazz); 92 if (!implementsIndexable) { 93 if (!notImplementingIndexableGrandfatherList.remove(className)) { 94 notImplementingIndexable.add(className); 95 } 96 continue; 97 } 98 final boolean hasSearchIndexProvider = hasSearchIndexProvider(clazz); 99 // If it implements Indexable, it must also implement the index provider field. 100 if (!hasSearchIndexProvider) { 101 if (!notImplementingIndexProviderGrandfatherList.remove(className)) { 102 notImplementingIndexProvider.add(className); 103 } 104 continue; 105 } 106 // If it implements index provider field AND it's a DashboardFragment, its fragment and 107 // search provider must share the same set of PreferenceControllers. 108 final boolean isSharingPrefControllers = DashboardFragmentSearchIndexProviderInspector 109 .isSharingPreferenceControllers(clazz); 110 if (!isSharingPrefControllers) { 111 if (!notSharingPrefControllersGrandfatherList.remove(className)) { 112 notSharingPreferenceControllers.add(className); 113 } 114 continue; 115 } 116 // Must be in SearchProviderRegistry 117 if (SearchIndexableResources.getResourceByName(className) == null) { 118 if (!notInSearchIndexableRegistryGrandfatherList.remove(className)) { 119 notInSearchProviderRegistry.add(className); 120 } 121 continue; 122 } 123 } 124 125 // Build error messages 126 final String indexableError = buildErrorMessage(NOT_IMPLEMENTING_INDEXABLE_ERROR, 127 notImplementingIndexable); 128 final String indexProviderError = buildErrorMessage(NOT_CONTAINING_PROVIDER_OBJECT_ERROR, 129 notImplementingIndexProvider); 130 final String notSharingPrefControllerError = buildErrorMessage( 131 NOT_SHARING_PREF_CONTROLLERS_BETWEEN_FRAG_AND_PROVIDER, 132 notSharingPreferenceControllers); 133 final String notInProviderRegistryError = 134 buildErrorMessage(NOT_IN_INDEXABLE_PROVIDER_REGISTRY, notInSearchProviderRegistry); 135 assertWithMessage(indexableError) 136 .that(notImplementingIndexable) 137 .isEmpty(); 138 assertWithMessage(indexProviderError) 139 .that(notImplementingIndexProvider) 140 .isEmpty(); 141 assertWithMessage(notSharingPrefControllerError) 142 .that(notSharingPreferenceControllers) 143 .isEmpty(); 144 assertWithMessage(notInProviderRegistryError) 145 .that(notInSearchProviderRegistry) 146 .isEmpty(); 147 assertNoObsoleteInGrandfatherList("grandfather_not_implementing_indexable", 148 notImplementingIndexableGrandfatherList); 149 assertNoObsoleteInGrandfatherList("grandfather_not_implementing_index_provider", 150 notImplementingIndexProviderGrandfatherList); 151 assertNoObsoleteInGrandfatherList("grandfather_not_in_search_index_provider_registry", 152 notInSearchIndexableRegistryGrandfatherList); 153 assertNoObsoleteInGrandfatherList( 154 "grandfather_not_sharing_pref_controllers_with_search_provider", 155 notSharingPrefControllersGrandfatherList); 156 } 157 158 private boolean hasSearchIndexProvider(Class clazz) { 159 try { 160 final Field f = clazz.getField( 161 DatabaseIndexingManager.FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); 162 return f != null; 163 } catch (NoClassDefFoundError e) { 164 // Cannot find class def, ignore 165 return true; 166 } catch (NoSuchFieldException e) { 167 Log.e(TAG, "error fetching search provider from class " + clazz.getName()); 168 return false; 169 } 170 } 171 172 private String buildErrorMessage(String errorSummary, Set<String> errorClasses) { 173 final StringBuilder error = new StringBuilder(errorSummary); 174 for (String c : errorClasses) { 175 error.append(c).append("\n"); 176 } 177 return error.toString(); 178 } 179} 180