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