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