Remove search2 package and put everything into search
Change-Id: Ie8651c79ef023f154cd7120a88a20f3481abce2b Fixes: 38502743 Test: make RunSettingsRoboTests
This commit is contained in:
353
src/com/android/settings/search/DatabaseResultLoader.java
Normal file
353
src/com/android/settings/search/DatabaseResultLoader.java
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.android.settings.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.text.TextUtils;
|
||||
import com.android.settings.dashboard.SiteMapManager;
|
||||
import com.android.settings.utils.AsyncLoader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns;
|
||||
import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
|
||||
|
||||
/**
|
||||
* AsyncTask to retrieve Settings, First party app and any intent based results.
|
||||
*/
|
||||
public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResult>> {
|
||||
private static final String LOG = "DatabaseResultLoader";
|
||||
|
||||
/* These indices are used to match the columns of the this loader's SELECT statement.
|
||||
These are not necessarily the same order nor similar coverage as the schema defined in
|
||||
IndexDatabaseHelper */
|
||||
static final int COLUMN_INDEX_ID = 0;
|
||||
static final int COLUMN_INDEX_TITLE = 1;
|
||||
static final int COLUMN_INDEX_SUMMARY_ON = 2;
|
||||
static final int COLUMN_INDEX_SUMMARY_OFF = 3;
|
||||
static final int COLUMN_INDEX_CLASS_NAME = 4;
|
||||
static final int COLUMN_INDEX_SCREEN_TITLE = 5;
|
||||
static final int COLUMN_INDEX_ICON = 6;
|
||||
static final int COLUMN_INDEX_INTENT_ACTION = 7;
|
||||
static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
|
||||
static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
|
||||
static final int COLUMN_INDEX_KEY = 10;
|
||||
static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
|
||||
static final int COLUMN_INDEX_PAYLOAD = 12;
|
||||
|
||||
public static final String[] SELECT_COLUMNS = {
|
||||
IndexColumns.DOCID,
|
||||
IndexColumns.DATA_TITLE,
|
||||
IndexColumns.DATA_SUMMARY_ON,
|
||||
IndexColumns.DATA_SUMMARY_OFF,
|
||||
IndexColumns.CLASS_NAME,
|
||||
IndexColumns.SCREEN_TITLE,
|
||||
IndexColumns.ICON,
|
||||
IndexColumns.INTENT_ACTION,
|
||||
IndexColumns.INTENT_TARGET_PACKAGE,
|
||||
IndexColumns.INTENT_TARGET_CLASS,
|
||||
IndexColumns.DATA_KEY_REF,
|
||||
IndexColumns.PAYLOAD_TYPE,
|
||||
IndexColumns.PAYLOAD
|
||||
};
|
||||
|
||||
public static final String[] MATCH_COLUMNS_PRIMARY = {
|
||||
IndexColumns.DATA_TITLE,
|
||||
IndexColumns.DATA_TITLE_NORMALIZED,
|
||||
};
|
||||
|
||||
public static final String[] MATCH_COLUMNS_SECONDARY = {
|
||||
IndexColumns.DATA_SUMMARY_ON,
|
||||
IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
|
||||
IndexColumns.DATA_SUMMARY_OFF,
|
||||
IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
|
||||
};
|
||||
|
||||
public static final String[] MATCH_COLUMNS_TERTIARY = {
|
||||
IndexColumns.DATA_KEYWORDS,
|
||||
IndexColumns.DATA_ENTRIES
|
||||
};
|
||||
|
||||
/**
|
||||
* Base ranks defines the best possible rank based on what the query matches.
|
||||
* If the query matches the prefix of the first word in the title, the best rank it can be is 1
|
||||
* If the query matches the prefix of the other words in the title, the best rank it can be is 3
|
||||
* If the query only matches the summary, the best rank it can be is 7
|
||||
* If the query only matches keywords or entries, the best rank it can be is 9
|
||||
*
|
||||
*/
|
||||
public static final int[] BASE_RANKS = {1, 3, 7, 9};
|
||||
|
||||
private final String mQueryText;
|
||||
private final Context mContext;
|
||||
private final CursorToSearchResultConverter mConverter;
|
||||
private final SiteMapManager mSiteMapManager;
|
||||
|
||||
public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
|
||||
super(context);
|
||||
mSiteMapManager = mapManager;
|
||||
mContext = context;
|
||||
mQueryText = cleanQuery(queryText);
|
||||
mConverter = new CursorToSearchResultConverter(context, mQueryText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDiscardResult(List<? extends SearchResult> result) {
|
||||
// TODO Search
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends SearchResult> loadInBackground() {
|
||||
if (mQueryText == null || mQueryText.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<SearchResult> primaryFirstWordResults;
|
||||
final List<SearchResult> primaryMidWordResults;
|
||||
final List<SearchResult> secondaryResults;
|
||||
final List<SearchResult> tertiaryResults;
|
||||
|
||||
primaryFirstWordResults = firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]);
|
||||
primaryMidWordResults = secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]);
|
||||
secondaryResults = anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]);
|
||||
tertiaryResults = anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]);
|
||||
|
||||
final List<SearchResult> results = new ArrayList<>(
|
||||
primaryFirstWordResults.size()
|
||||
+ primaryMidWordResults.size()
|
||||
+ secondaryResults.size()
|
||||
+ tertiaryResults.size());
|
||||
|
||||
results.addAll(primaryFirstWordResults);
|
||||
results.addAll(primaryMidWordResults);
|
||||
results.addAll(secondaryResults);
|
||||
results.addAll(tertiaryResults);
|
||||
|
||||
return removeDuplicates(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onCancelLoad() {
|
||||
// TODO
|
||||
return super.onCancelLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic method to make the query suitable for searching the database.
|
||||
*
|
||||
* @return the cleaned query string
|
||||
*/
|
||||
private static String cleanQuery(String query) {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
return null;
|
||||
}
|
||||
return query.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and executes the query which matches prefixes of the first word of the given columns.
|
||||
*
|
||||
* @param matchColumns The columns to match on
|
||||
* @param baseRank The highest rank achievable by these results
|
||||
* @return A list of the matching results.
|
||||
*/
|
||||
private List<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
|
||||
final String whereClause = buildSingleWordWhereClause(matchColumns);
|
||||
final String query = mQueryText + "%";
|
||||
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
|
||||
|
||||
return query(whereClause, selection, baseRank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and executes the query which matches prefixes of the non-first words of the
|
||||
* given columns.
|
||||
*
|
||||
* @param matchColumns The columns to match on
|
||||
* @param baseRank The highest rank achievable by these results
|
||||
* @return A list of the matching results.
|
||||
*/
|
||||
private List<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) {
|
||||
final String whereClause = buildSingleWordWhereClause(matchColumns);
|
||||
final String query = "% " + mQueryText + "%";
|
||||
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
|
||||
|
||||
return query(whereClause, selection, baseRank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and executes the query which matches prefixes of the any word of the given columns.
|
||||
*
|
||||
* @param matchColumns The columns to match on
|
||||
* @param baseRank The highest rank achievable by these results
|
||||
* @return A list of the matching results.
|
||||
*/
|
||||
private List<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
|
||||
final String whereClause = buildTwoWordWhereClause(matchColumns);
|
||||
final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
|
||||
|
||||
return query(whereClause, selection, baseRank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method used by all of the query methods above to execute a query.
|
||||
*
|
||||
* @param whereClause Where clause for the SQL query which uses bindings.
|
||||
* @param selection List of the transformed query to match each bind in the whereClause
|
||||
* @param baseRank The highest rank achievable by these results.
|
||||
* @return A list of the matching results.
|
||||
*/
|
||||
private List<SearchResult> query(String whereClause, String[] selection, int baseRank) {
|
||||
final SQLiteDatabase database = IndexDatabaseHelper.getInstance(mContext)
|
||||
.getReadableDatabase();
|
||||
final Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause,
|
||||
selection, null, null, null);
|
||||
return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQLite WHERE clause that matches all matchColumns for a single query.
|
||||
*
|
||||
* @param matchColumns List of columns that will be used for matching.
|
||||
* @return The constructed WHERE clause.
|
||||
*/
|
||||
private static String buildSingleWordWhereClause(String[] matchColumns) {
|
||||
StringBuilder sb = new StringBuilder(" (");
|
||||
final int count = matchColumns.length;
|
||||
for (int n = 0; n < count; n++) {
|
||||
sb.append(matchColumns[n]);
|
||||
sb.append(" like ? ");
|
||||
if (n < count - 1) {
|
||||
sb.append(" OR ");
|
||||
}
|
||||
}
|
||||
sb.append(") AND enabled = 1");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the SQLite WHERE clause that matches all matchColumns to two different queries.
|
||||
*
|
||||
* @param matchColumns List of columns that will be used for matching.
|
||||
* @return The constructed WHERE clause.
|
||||
*/
|
||||
private static String buildTwoWordWhereClause(String[] matchColumns) {
|
||||
StringBuilder sb = new StringBuilder(" (");
|
||||
final int count = matchColumns.length;
|
||||
for (int n = 0; n < count; n++) {
|
||||
sb.append(matchColumns[n]);
|
||||
sb.append(" like ? OR ");
|
||||
sb.append(matchColumns[n]);
|
||||
sb.append(" like ?");
|
||||
if (n < count - 1) {
|
||||
sb.append(" OR ");
|
||||
}
|
||||
}
|
||||
sb.append(") AND enabled = 1");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills out the selection array to match the query as the prefix of a single word.
|
||||
*
|
||||
* @param size is the number of columns to be matched.
|
||||
*/
|
||||
private String[] buildSingleWordSelection(String query, int size) {
|
||||
String[] selection = new String[size];
|
||||
|
||||
for(int i = 0; i < size; i ++) {
|
||||
selection[i] = query;
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills out the selection array to match the query as the prefix of a word.
|
||||
*
|
||||
* @param size is twice the number of columns to be matched. The first match is for the prefix
|
||||
* of the first word in the column. The second match is for any subsequent word
|
||||
* prefix match.
|
||||
*/
|
||||
private String[] buildAnyWordSelection(int size) {
|
||||
String[] selection = new String[size];
|
||||
final String query = mQueryText + "%";
|
||||
final String subStringQuery = "% " + mQueryText + "%";
|
||||
|
||||
for(int i = 0; i < (size - 1); i += 2) {
|
||||
selection[i] = query;
|
||||
selection[i + 1] = subStringQuery;
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through the list of search results and verifies that none of the results are duplicates.
|
||||
* A duplicate is quantified by a result with the same Title and the same non-empty Summary.
|
||||
*
|
||||
* The method walks through the results starting with the highest priority result. It removes
|
||||
* the duplicates by doing the first rule that applies below:
|
||||
* - If a result is inline, remove the intent result.
|
||||
* - Remove the lower rank item.
|
||||
* @param results A list of results with potential duplicates
|
||||
* @return The list of results with duplicates removed.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
List<SearchResult> removeDuplicates(List<SearchResult> results) {
|
||||
SearchResult primaryResult, secondaryResult;
|
||||
|
||||
// We accept the O(n^2) solution because the number of results is small.
|
||||
for (int i = results.size() - 1; i >= 0; i--) {
|
||||
secondaryResult = results.get(i);
|
||||
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
primaryResult = results.get(j);
|
||||
if (areDuplicateResults(primaryResult, secondaryResult)) {
|
||||
|
||||
if (primaryResult.viewType != ResultPayload.PayloadType.INTENT) {
|
||||
// Case where both payloads are inline
|
||||
results.remove(i);
|
||||
break;
|
||||
} else if (secondaryResult.viewType != ResultPayload.PayloadType.INTENT) {
|
||||
// Case where only second result is inline
|
||||
results.remove(j);
|
||||
i--; // shift the top index to reflect the lower element being removed
|
||||
} else {
|
||||
// Case where both payloads are intent
|
||||
results.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True when the two {@link SearchResult SearchResults} have the same title, and the same
|
||||
* non-empty summary.
|
||||
*/
|
||||
private boolean areDuplicateResults(SearchResult primary, SearchResult secondary) {
|
||||
return TextUtils.equals(primary.title, secondary.title)
|
||||
&& TextUtils.equals(primary.summary, secondary.summary)
|
||||
&& !TextUtils.isEmpty(primary.summary);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user