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
17package com.android.settings.search;
18
19import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
20import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
21import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
22import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
23import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
24import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
25import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
26import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
27import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
28import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
29import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
30import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
31import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
32import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
33import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
34import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
35import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
36import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
37import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
38import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
39import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
40import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS;
41import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS;
42import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS;
43import static android.provider.SearchIndexablesContract.SITE_MAP_COLUMNS;
44
45import static com.android.settings.dashboard.DashboardFragmentRegistry.CATEGORY_KEY_TO_PARENT_MAP;
46
47import android.content.Context;
48import android.database.Cursor;
49import android.database.MatrixCursor;
50import android.provider.SearchIndexableResource;
51import android.provider.SearchIndexablesContract;
52import android.provider.SearchIndexablesProvider;
53import android.text.TextUtils;
54import android.util.ArraySet;
55import android.util.Log;
56
57import com.android.settings.SettingsActivity;
58import com.android.settings.overlay.FeatureFactory;
59import com.android.settingslib.drawer.DashboardCategory;
60import com.android.settingslib.drawer.Tile;
61
62import java.util.ArrayList;
63import java.util.Collection;
64import java.util.List;
65
66public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
67
68    public static final boolean DEBUG = false;
69
70    /**
71     * Flag for a system property which checks if we should crash if there are issues in the
72     * indexing pipeline.
73     */
74    public static final String SYSPROP_CRASH_ON_ERROR =
75            "debug.com.android.settings.search.crash_on_error";
76
77    private static final String TAG = "SettingsSearchProvider";
78
79    private static final Collection<String> INVALID_KEYS;
80
81    static {
82        INVALID_KEYS = new ArraySet<>();
83        INVALID_KEYS.add(null);
84        INVALID_KEYS.add("");
85    }
86
87    @Override
88    public boolean onCreate() {
89        return true;
90    }
91
92    @Override
93    public Cursor queryXmlResources(String[] projection) {
94        MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
95        final List<SearchIndexableResource> resources =
96                getSearchIndexableResourcesFromProvider(getContext());
97        for (SearchIndexableResource val : resources) {
98            Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
99            ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
100            ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
101            ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
102            ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
103            ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
104            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
105            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
106            cursor.addRow(ref);
107        }
108
109        return cursor;
110    }
111
112    @Override
113    public Cursor queryRawData(String[] projection) {
114        MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
115        final List<SearchIndexableRaw> raws = getSearchIndexableRawFromProvider(getContext());
116        for (SearchIndexableRaw val : raws) {
117            Object[] ref = new Object[INDEXABLES_RAW_COLUMNS.length];
118            ref[COLUMN_INDEX_RAW_TITLE] = val.title;
119            ref[COLUMN_INDEX_RAW_SUMMARY_ON] = val.summaryOn;
120            ref[COLUMN_INDEX_RAW_SUMMARY_OFF] = val.summaryOff;
121            ref[COLUMN_INDEX_RAW_ENTRIES] = val.entries;
122            ref[COLUMN_INDEX_RAW_KEYWORDS] = val.keywords;
123            ref[COLUMN_INDEX_RAW_SCREEN_TITLE] = val.screenTitle;
124            ref[COLUMN_INDEX_RAW_CLASS_NAME] = val.className;
125            ref[COLUMN_INDEX_RAW_ICON_RESID] = val.iconResId;
126            ref[COLUMN_INDEX_RAW_INTENT_ACTION] = val.intentAction;
127            ref[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
128            ref[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = val.intentTargetClass;
129            ref[COLUMN_INDEX_RAW_KEY] = val.key;
130            ref[COLUMN_INDEX_RAW_USER_ID] = val.userId;
131            cursor.addRow(ref);
132        }
133
134        return cursor;
135    }
136
137    /**
138     * Gets a combined list non-indexable keys that come from providers inside of settings.
139     * The non-indexable keys are used in Settings search at both index and update time to verify
140     * the validity of results in the database.
141     */
142    @Override
143    public Cursor queryNonIndexableKeys(String[] projection) {
144        MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
145        final List<String> nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
146        for (String nik : nonIndexableKeys) {
147            final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
148            ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
149            cursor.addRow(ref);
150        }
151
152        return cursor;
153    }
154
155    @Override
156    public Cursor querySiteMapPairs() {
157        final MatrixCursor cursor = new MatrixCursor(SITE_MAP_COLUMNS);
158        final Context context = getContext();
159        // Loop through all IA categories and pages and build additional SiteMapPairs
160        final List<DashboardCategory> categories = FeatureFactory.getFactory(context)
161                .getDashboardFeatureProvider(context).getAllCategories();
162        for (DashboardCategory category : categories) {
163            // Use the category key to look up parent (which page hosts this key)
164            final String parentClass = CATEGORY_KEY_TO_PARENT_MAP.get(category.key);
165            if (parentClass == null) {
166                continue;
167            }
168            // Build parent-child class pairs for all children listed under this key.
169            for (Tile tile : category.getTiles()) {
170                String childClass = null;
171                if (tile.metaData != null) {
172                    childClass = tile.metaData.getString(
173                            SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
174                }
175                if (childClass == null) {
176                    continue;
177                }
178                cursor.newRow()
179                        .add(SearchIndexablesContract.SiteMapColumns.PARENT_CLASS, parentClass)
180                        .add(SearchIndexablesContract.SiteMapColumns.CHILD_CLASS, childClass);
181            }
182        }
183        // Done.
184        return cursor;
185    }
186
187    private List<String> getNonIndexableKeysFromProvider(Context context) {
188        final Collection<Class> values = FeatureFactory.getFactory(context)
189                .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
190        final List<String> nonIndexableKeys = new ArrayList<>();
191
192        for (Class<?> clazz : values) {
193            final long startTime = System.currentTimeMillis();
194            Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
195                    clazz);
196
197            List<String> providerNonIndexableKeys;
198            try {
199                providerNonIndexableKeys = provider.getNonIndexableKeys(context);
200            } catch (Exception e) {
201                // Catch a generic crash. In the absence of the catch, the background thread will
202                // silently fail anyway, so we aren't losing information by catching the exception.
203                // We crash when the system property exists so that we can test if crashes need to
204                // be fixed.
205                // The gain is that if there is a crash in a specific controller, we don't lose all
206                // non-indexable keys, but we can still find specific crashes in development.
207                if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
208                    throw new RuntimeException(e);
209                }
210                Log.e(TAG, "Error trying to get non-indexable keys from: " + clazz.getName() , e);
211                continue;
212            }
213
214            if (providerNonIndexableKeys == null || providerNonIndexableKeys.isEmpty()) {
215                if (DEBUG) {
216                    final long totalTime = System.currentTimeMillis() - startTime;
217                    Log.d(TAG, "No indexable, total time " + totalTime);
218                }
219                continue;
220            }
221
222            if (providerNonIndexableKeys.removeAll(INVALID_KEYS)) {
223                Log.v(TAG, provider + " tried to add an empty non-indexable key");
224            }
225
226            if (DEBUG) {
227                final long totalTime = System.currentTimeMillis() - startTime;
228                Log.d(TAG, "Non-indexables " + providerNonIndexableKeys.size() + ", total time "
229                        + totalTime);
230            }
231
232            nonIndexableKeys.addAll(providerNonIndexableKeys);
233        }
234
235        return nonIndexableKeys;
236    }
237
238    private List<SearchIndexableResource> getSearchIndexableResourcesFromProvider(Context context) {
239        Collection<Class> values = FeatureFactory.getFactory(context)
240                .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
241        List<SearchIndexableResource> resourceList = new ArrayList<>();
242
243        for (Class<?> clazz : values) {
244            Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
245                    clazz);
246
247            final List<SearchIndexableResource> resList =
248                    provider.getXmlResourcesToIndex(context, true);
249
250            if (resList == null) {
251                continue;
252            }
253
254            for (SearchIndexableResource item : resList) {
255                item.className = TextUtils.isEmpty(item.className)
256                        ? clazz.getName()
257                        : item.className;
258            }
259
260            resourceList.addAll(resList);
261        }
262
263        return resourceList;
264    }
265
266    private List<SearchIndexableRaw> getSearchIndexableRawFromProvider(Context context) {
267        final Collection<Class> values = FeatureFactory.getFactory(context)
268                .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
269        final List<SearchIndexableRaw> rawList = new ArrayList<>();
270
271        for (Class<?> clazz : values) {
272            Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider(
273                    clazz);
274            final List<SearchIndexableRaw> providerRaws = provider.getRawDataToIndex(context,
275                    true /* enabled */);
276
277            if (providerRaws == null) {
278                continue;
279            }
280
281            for (SearchIndexableRaw raw : providerRaws) {
282                // The classname and intent information comes from the PreIndexData
283                // This will be more clear when provider conversion is done at PreIndex time.
284                raw.className = clazz.getName();
285
286            }
287            rawList.addAll(providerRaws);
288        }
289
290        return rawList;
291    }
292}
293