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