• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 
17 package com.android.settings.intelligence.search.query;
18 
19 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.IndexColumns;
20 import static com.android.settings.intelligence.search.indexing.IndexDatabaseHelper.Tables
21         .TABLE_PREFS_INDEX;
22 
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.support.annotation.VisibleForTesting;
27 import android.util.Log;
28 import android.util.Pair;
29 
30 import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
31 import com.android.settings.intelligence.overlay.FeatureFactory;
32 import com.android.settings.intelligence.search.SearchFeatureProvider;
33 import com.android.settings.intelligence.search.SearchResult;
34 import com.android.settings.intelligence.search.indexing.IndexDatabaseHelper;
35 import com.android.settings.intelligence.search.sitemap.SiteMapManager;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43 import java.util.TreeSet;
44 import java.util.concurrent.ExecutionException;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.FutureTask;
47 import java.util.concurrent.TimeUnit;
48 import java.util.concurrent.TimeoutException;
49 
50 /**
51  * AsyncTask to retrieve Settings, first party app and any intent based results.
52  */
53 public class DatabaseResultTask extends SearchQueryTask.QueryWorker {
54 
55     private static final String TAG = "DatabaseResultTask";
56 
57     public static final String[] SELECT_COLUMNS = {
58             IndexColumns.DATA_TITLE,
59             IndexColumns.DATA_SUMMARY_ON,
60             IndexColumns.DATA_SUMMARY_OFF,
61             IndexColumns.CLASS_NAME,
62             IndexColumns.SCREEN_TITLE,
63             IndexColumns.ICON,
64             IndexColumns.INTENT_ACTION,
65             IndexColumns.DATA_PACKAGE,
66             IndexColumns.INTENT_TARGET_PACKAGE,
67             IndexColumns.INTENT_TARGET_CLASS,
68             IndexColumns.DATA_KEY_REF,
69             IndexColumns.PAYLOAD_TYPE,
70             IndexColumns.PAYLOAD
71     };
72 
73     public static final String[] MATCH_COLUMNS_PRIMARY = {
74             IndexColumns.DATA_TITLE,
75             IndexColumns.DATA_TITLE_NORMALIZED,
76     };
77 
78     public static final String[] MATCH_COLUMNS_SECONDARY = {
79             IndexColumns.DATA_SUMMARY_ON,
80             IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
81             IndexColumns.DATA_SUMMARY_OFF,
82             IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
83     };
84 
85     public static final int QUERY_WORKER_ID =
86             SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_DATABASE;
87 
88     /**
89      * Base ranks defines the best possible rank based on what the query matches.
90      * If the query matches the prefix of the first word in the title, the best rank it can be
91      * is 1
92      * If the query matches the prefix of the other words in the title, the best rank it can be
93      * is 3
94      * If the query only matches the summary, the best rank it can be is 7
95      * If the query only matches keywords or entries, the best rank it can be is 9
96      */
97     static final int[] BASE_RANKS = {1, 3, 7, 9};
98 
newTask(Context context, SiteMapManager siteMapManager, String query)99     public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
100             String query) {
101         return new SearchQueryTask(new DatabaseResultTask(context, siteMapManager, query));
102     }
103 
104     public final String[] MATCH_COLUMNS_TERTIARY = {
105             IndexColumns.DATA_KEYWORDS,
106             IndexColumns.DATA_ENTRIES
107     };
108 
109     private final CursorToSearchResultConverter mConverter;
110     private final SearchFeatureProvider mFeatureProvider;
111 
DatabaseResultTask(Context context, SiteMapManager siteMapManager, String queryText)112     public DatabaseResultTask(Context context, SiteMapManager siteMapManager, String queryText) {
113         super(context, siteMapManager, queryText);
114         mConverter = new CursorToSearchResultConverter(context);
115         mFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
116     }
117 
118     @Override
getQueryWorkerId()119     protected int getQueryWorkerId() {
120         return QUERY_WORKER_ID;
121     }
122 
123     @Override
query()124     protected List<? extends SearchResult> query() {
125         if (mQuery == null || mQuery.isEmpty()) {
126             return new ArrayList<>();
127         }
128         // Start a Future to get search result scores.
129         FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
130                 mContext, mQuery);
131 
132         if (rankerTask != null) {
133             ExecutorService executorService = mFeatureProvider.getExecutorService();
134             executorService.execute(rankerTask);
135         }
136 
137         final Set<SearchResult> resultSet = new HashSet<>();
138 
139         resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
140         resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
141         resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
142         resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
143 
144         // Try to retrieve the scores in time. Otherwise use static ranking.
145         if (rankerTask != null) {
146             try {
147                 final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
148                 List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
149                         TimeUnit.MILLISECONDS);
150                 return getDynamicRankedResults(resultSet, searchRankScores);
151             } catch (TimeoutException | InterruptedException | ExecutionException e) {
152                 Log.d(TAG, "Error waiting for result scores: " + e);
153             }
154         }
155 
156         List<SearchResult> resultList = new ArrayList<>(resultSet);
157         Collections.sort(resultList);
158         return resultList;
159     }
160 
161     // TODO (b/33577327) Retrieve all search results with a single query.
162 
163     /**
164      * Creates and executes the query which matches prefixes of the first word of the given
165      * columns.
166      *
167      * @param matchColumns The columns to match on
168      * @param baseRank     The highest rank achievable by these results
169      * @return A set of the matching results.
170      */
firstWordQuery(String[] matchColumns, int baseRank)171     private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
172         final String whereClause = buildSingleWordWhereClause(matchColumns);
173         final String query = mQuery + "%";
174         final String[] selection = buildSingleWordSelection(query, matchColumns.length);
175 
176         return query(whereClause, selection, baseRank);
177     }
178 
179     /**
180      * Creates and executes the query which matches prefixes of the non-first words of the
181      * given columns.
182      *
183      * @param matchColumns The columns to match on
184      * @param baseRank     The highest rank achievable by these results
185      * @return A set of the matching results.
186      */
secondaryWordQuery(String[] matchColumns, int baseRank)187     private Set<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) {
188         final String whereClause = buildSingleWordWhereClause(matchColumns);
189         final String query = "% " + mQuery + "%";
190         final String[] selection = buildSingleWordSelection(query, matchColumns.length);
191 
192         return query(whereClause, selection, baseRank);
193     }
194 
195     /**
196      * Creates and executes the query which matches prefixes of the any word of the given
197      * columns.
198      *
199      * @param matchColumns The columns to match on
200      * @param baseRank     The highest rank achievable by these results
201      * @return A set of the matching results.
202      */
anyWordQuery(String[] matchColumns, int baseRank)203     private Set<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
204         final String whereClause = buildTwoWordWhereClause(matchColumns);
205         final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
206 
207         return query(whereClause, selection, baseRank);
208     }
209 
210     /**
211      * Generic method used by all of the query methods above to execute a query.
212      *
213      * @param whereClause Where clause for the SQL query which uses bindings.
214      * @param selection   List of the transformed query to match each bind in the whereClause
215      * @param baseRank    The highest rank achievable by these results.
216      * @return A set of the matching results.
217      */
query(String whereClause, String[] selection, int baseRank)218     private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
219         final SQLiteDatabase database =
220                 IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
221         try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
222                 whereClause,
223                 selection, null, null, null)) {
224             return mConverter.convertCursor(resultCursor, baseRank, mSiteMapManager);
225         }
226     }
227 
228     /**
229      * Builds the SQLite WHERE clause that matches all matchColumns for a single query.
230      *
231      * @param matchColumns List of columns that will be used for matching.
232      * @return The constructed WHERE clause.
233      */
buildSingleWordWhereClause(String[] matchColumns)234     private static String buildSingleWordWhereClause(String[] matchColumns) {
235         StringBuilder sb = new StringBuilder(" (");
236         final int count = matchColumns.length;
237         for (int n = 0; n < count; n++) {
238             sb.append(matchColumns[n]);
239             sb.append(" like ? ");
240             if (n < count - 1) {
241                 sb.append(" OR ");
242             }
243         }
244         sb.append(") AND enabled = 1");
245         return sb.toString();
246     }
247 
248     /**
249      * Builds the SQLite WHERE clause that matches all matchColumns to two different queries.
250      *
251      * @param matchColumns List of columns that will be used for matching.
252      * @return The constructed WHERE clause.
253      */
buildTwoWordWhereClause(String[] matchColumns)254     private static String buildTwoWordWhereClause(String[] matchColumns) {
255         StringBuilder sb = new StringBuilder(" (");
256         final int count = matchColumns.length;
257         for (int n = 0; n < count; n++) {
258             sb.append(matchColumns[n]);
259             sb.append(" like ? OR ");
260             sb.append(matchColumns[n]);
261             sb.append(" like ?");
262             if (n < count - 1) {
263                 sb.append(" OR ");
264             }
265         }
266         sb.append(") AND enabled = 1");
267         return sb.toString();
268     }
269 
270     /**
271      * Fills out the selection array to match the query as the prefix of a single word.
272      *
273      * @param size is the number of columns to be matched.
274      */
buildSingleWordSelection(String query, int size)275     private String[] buildSingleWordSelection(String query, int size) {
276         String[] selection = new String[size];
277 
278         for (int i = 0; i < size; i++) {
279             selection[i] = query;
280         }
281         return selection;
282     }
283 
284     /**
285      * Fills out the selection array to match the query as the prefix of a word.
286      *
287      * @param size is twice the number of columns to be matched. The first match is for the
288      *             prefix
289      *             of the first word in the column. The second match is for any subsequent word
290      *             prefix match.
291      */
buildAnyWordSelection(int size)292     private String[] buildAnyWordSelection(int size) {
293         String[] selection = new String[size];
294         final String query = mQuery + "%";
295         final String subStringQuery = "% " + mQuery + "%";
296 
297         for (int i = 0; i < (size - 1); i += 2) {
298             selection[i] = query;
299             selection[i + 1] = subStringQuery;
300         }
301         return selection;
302     }
303 
getDynamicRankedResults(Set<SearchResult> unsortedSet, final List<Pair<String, Float>> searchRankScores)304     private List<SearchResult> getDynamicRankedResults(Set<SearchResult> unsortedSet,
305             final List<Pair<String, Float>> searchRankScores) {
306         final TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
307                 new Comparator<SearchResult>() {
308                     @Override
309                     public int compare(SearchResult o1, SearchResult o2) {
310                         final float score1 = getRankingScoreByKey(searchRankScores, o1.dataKey);
311                         final float score2 = getRankingScoreByKey(searchRankScores, o2.dataKey);
312                         if (score1 > score2) {
313                             return -1;
314                         } else {
315                             return 1;
316                         }
317                     }
318                 });
319         dbResultsSortedByScores.addAll(unsortedSet);
320 
321         return new ArrayList<>(dbResultsSortedByScores);
322     }
323 
324     /**
325      * Looks up ranking score by key.
326      *
327      * @param key key for a single search result.
328      * @return the ranking score corresponding to the given stableId. If there is no score
329      * available for this stableId, -Float.MAX_VALUE is returned.
330      */
331     @VisibleForTesting
getRankingScoreByKey(List<Pair<String, Float>> searchRankScores, String key)332     Float getRankingScoreByKey(List<Pair<String, Float>> searchRankScores, String key) {
333         for (Pair<String, Float> rankingScore : searchRankScores) {
334             if (key.compareTo(rankingScore.first) == 0) {
335                 return rankingScore.second;
336             }
337         }
338         // If key not found in the list, we assign the minimum score so it will appear at
339         // the end of the list.
340         Log.w(TAG, key + " was not in the ranking scores.");
341         return -Float.MAX_VALUE;
342     }
343 }