Loading src/com/android/settings/search2/DatabaseResultLoader.java +52 −1 Original line number Diff line number Diff line Loading @@ -144,7 +144,7 @@ public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResul results.addAll(secondaryResults); results.addAll(tertiaryResults); return results; return removeDuplicates(results); } @Override Loading Loading @@ -300,4 +300,55 @@ public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResul } 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); } } No newline at end of file tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java +190 −12 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.settings.search2; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import com.android.settings.SettingsRobolectricTestRunner; Loading @@ -38,6 +39,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.List; import static com.google.common.truth.Truth.assertThat; Loading @@ -58,6 +60,15 @@ public class DatabaseResultLoaderTest { private Context mContext; private DatabaseResultLoader loader; private final String titleOne = "titleOne"; private final String titleTwo = "titleTwo"; private final String titleThree = "titleThree"; private final String titleFour = "titleFour"; private final String summaryOne = "summaryOne"; private final String summaryTwo = "summaryTwo"; private final String summaryThree = "summaryThree"; private final String summaryFour = "summaryFour"; SQLiteDatabase mDb; @Before Loading Loading @@ -104,49 +115,49 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseWord_MatchesNonPrefix() { public void testSpecialCaseWord_matchesNonPrefix() { insertSpecialCase("Data usage"); loader = new DatabaseResultLoader(mContext, "usage", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseSpace_Matches() { public void testSpecialCaseSpace_matches() { insertSpecialCase("space"); loader = new DatabaseResultLoader(mContext, " space ", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordNoDash() { public void testSpecialCaseDash_matchesWordNoDash() { insertSpecialCase("wi-fi calling"); loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordWithDash() { public void testSpecialCaseDash_matchesWordWithDash() { insertSpecialCase("priorités seulment"); loader = new DatabaseResultLoader(mContext, "priorités", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordWithoutDash() { public void testSpecialCaseDash_matchesWordWithoutDash() { insertSpecialCase("priorités seulment"); loader = new DatabaseResultLoader(mContext, "priorites", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesEntireQueryWithoutDash() { public void testSpecialCaseDash_matchesEntireQueryWithoutDash() { insertSpecialCase("wi-fi calling"); loader = new DatabaseResultLoader(mContext, "wifi calling", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCasePrefix_MatchesPrefixOfEntry() { public void testSpecialCasePrefix_matchesPrefixOfEntry() { insertSpecialCase("Photos"); loader = new DatabaseResultLoader(mContext, "pho", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); Loading @@ -160,14 +171,14 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseMultiWordPrefix_MatchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() { insertSpecialCase("Apps Notifications"); loader = new DatabaseResultLoader(mContext, "Apps", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseMultiWordPrefix_MatchesSecondWordPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() { insertSpecialCase("Apps Notifications"); loader = new DatabaseResultLoader(mContext, "Not", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); Loading @@ -188,21 +199,188 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() { insertSpecialCase("Apps & Notifications"); loader = new DatabaseResultLoader(mContext, "App", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfSecondEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() { insertSpecialCase("Apps & Notifications"); loader = new DatabaseResultLoader(mContext, "No", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseTwoWords_FirstWordMatches_RanksHigher() { public void testDeDupe_noDuplicates_originalListReturn() { // Three elements with unique titles and summaries List<SearchResult> results = new ArrayList(); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); builder.addTitle(titleOne) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); builder.addTitle(titleTwo) .addSummary(summaryTwo); SearchResult resultTwo = builder.build(); results.add(resultTwo); builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(3); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultTwo); assertThat(results.get(2)).isEqualTo(resultThree); } @Test public void testDeDupe_oneDuplicate_duplicateRemoved() { List<SearchResult> results = new ArrayList(); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(0) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Duplicate of the first element builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(1); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Unique builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(2); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); } @Test public void testDeDupe_firstDupeInline_secondDuplicateRemoved() { List<SearchResult> results = new ArrayList(); InlineSwitchPayload inlinePayload = new InlineSwitchPayload("", 0, null); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); // Inline result builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(0) .addPayload(inlinePayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Duplicate of first result, but Intent Result. Should be removed. builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(1) .addPayload(intentPayload); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Unique builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(2); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); } @Test public void testDeDupe_secondDupeInline_firstDuplicateRemoved() { /* * Create a list as follows: * (5) Intent Four * (4) Inline Two * (3) Intent Three * (2) Intent Two * (1) Intent One * * After removing duplicates: * (4) Intent Four * (3) Inline Two * (2) Intent Three * (1) Intent One */ List<SearchResult> results = new ArrayList(); InlineSwitchPayload inlinePayload = new InlineSwitchPayload("", 0, null); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); // Intent One builder.addTitle(titleOne) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Intent Two builder.addTitle(titleTwo) .addSummary(summaryTwo) .addPayload(intentPayload); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Intent Three builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); // Inline Two builder.addTitle(titleTwo) .addSummary(summaryTwo) .addPayload(inlinePayload); SearchResult resultFour = builder.build(); results.add(resultFour); // Intent Four builder.addTitle(titleFour) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultFive = builder.build(); results.add(resultFive); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(4); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); assertThat(results.get(2)).isEqualTo(resultFour); assertThat(results.get(3)).isEqualTo(resultFive); } @Test public void testSpecialCaseTwoWords_firstWordMatches_ranksHigher() { final String caseOne = "Apple pear"; final String caseTwo = "Banana apple"; insertSpecialCase(caseOne); Loading Loading
src/com/android/settings/search2/DatabaseResultLoader.java +52 −1 Original line number Diff line number Diff line Loading @@ -144,7 +144,7 @@ public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResul results.addAll(secondaryResults); results.addAll(tertiaryResults); return results; return removeDuplicates(results); } @Override Loading Loading @@ -300,4 +300,55 @@ public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResul } 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); } } No newline at end of file
tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java +190 −12 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.settings.search2; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import com.android.settings.SettingsRobolectricTestRunner; Loading @@ -38,6 +39,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.List; import static com.google.common.truth.Truth.assertThat; Loading @@ -58,6 +60,15 @@ public class DatabaseResultLoaderTest { private Context mContext; private DatabaseResultLoader loader; private final String titleOne = "titleOne"; private final String titleTwo = "titleTwo"; private final String titleThree = "titleThree"; private final String titleFour = "titleFour"; private final String summaryOne = "summaryOne"; private final String summaryTwo = "summaryTwo"; private final String summaryThree = "summaryThree"; private final String summaryFour = "summaryFour"; SQLiteDatabase mDb; @Before Loading Loading @@ -104,49 +115,49 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseWord_MatchesNonPrefix() { public void testSpecialCaseWord_matchesNonPrefix() { insertSpecialCase("Data usage"); loader = new DatabaseResultLoader(mContext, "usage", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseSpace_Matches() { public void testSpecialCaseSpace_matches() { insertSpecialCase("space"); loader = new DatabaseResultLoader(mContext, " space ", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordNoDash() { public void testSpecialCaseDash_matchesWordNoDash() { insertSpecialCase("wi-fi calling"); loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordWithDash() { public void testSpecialCaseDash_matchesWordWithDash() { insertSpecialCase("priorités seulment"); loader = new DatabaseResultLoader(mContext, "priorités", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesWordWithoutDash() { public void testSpecialCaseDash_matchesWordWithoutDash() { insertSpecialCase("priorités seulment"); loader = new DatabaseResultLoader(mContext, "priorites", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseDash_MatchesEntireQueryWithoutDash() { public void testSpecialCaseDash_matchesEntireQueryWithoutDash() { insertSpecialCase("wi-fi calling"); loader = new DatabaseResultLoader(mContext, "wifi calling", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCasePrefix_MatchesPrefixOfEntry() { public void testSpecialCasePrefix_matchesPrefixOfEntry() { insertSpecialCase("Photos"); loader = new DatabaseResultLoader(mContext, "pho", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); Loading @@ -160,14 +171,14 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseMultiWordPrefix_MatchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() { insertSpecialCase("Apps Notifications"); loader = new DatabaseResultLoader(mContext, "Apps", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseMultiWordPrefix_MatchesSecondWordPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() { insertSpecialCase("Apps Notifications"); loader = new DatabaseResultLoader(mContext, "Not", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); Loading @@ -188,21 +199,188 @@ public class DatabaseResultLoaderTest { } @Test public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() { insertSpecialCase("Apps & Notifications"); loader = new DatabaseResultLoader(mContext, "App", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfSecondEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() { insertSpecialCase("Apps & Notifications"); loader = new DatabaseResultLoader(mContext, "No", mSiteMapManager); assertThat(loader.loadInBackground().size()).isEqualTo(1); } @Test public void testSpecialCaseTwoWords_FirstWordMatches_RanksHigher() { public void testDeDupe_noDuplicates_originalListReturn() { // Three elements with unique titles and summaries List<SearchResult> results = new ArrayList(); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); builder.addTitle(titleOne) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); builder.addTitle(titleTwo) .addSummary(summaryTwo); SearchResult resultTwo = builder.build(); results.add(resultTwo); builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(3); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultTwo); assertThat(results.get(2)).isEqualTo(resultThree); } @Test public void testDeDupe_oneDuplicate_duplicateRemoved() { List<SearchResult> results = new ArrayList(); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(0) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Duplicate of the first element builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(1); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Unique builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(2); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); } @Test public void testDeDupe_firstDupeInline_secondDuplicateRemoved() { List<SearchResult> results = new ArrayList(); InlineSwitchPayload inlinePayload = new InlineSwitchPayload("", 0, null); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); // Inline result builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(0) .addPayload(inlinePayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Duplicate of first result, but Intent Result. Should be removed. builder.addTitle(titleOne) .addSummary(summaryOne) .addRank(1) .addPayload(intentPayload); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Unique builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(2); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); } @Test public void testDeDupe_secondDupeInline_firstDuplicateRemoved() { /* * Create a list as follows: * (5) Intent Four * (4) Inline Two * (3) Intent Three * (2) Intent Two * (1) Intent One * * After removing duplicates: * (4) Intent Four * (3) Inline Two * (2) Intent Three * (1) Intent One */ List<SearchResult> results = new ArrayList(); InlineSwitchPayload inlinePayload = new InlineSwitchPayload("", 0, null); IntentPayload intentPayload = new IntentPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); // Intent One builder.addTitle(titleOne) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultOne = builder.build(); results.add(resultOne); // Intent Two builder.addTitle(titleTwo) .addSummary(summaryTwo) .addPayload(intentPayload); SearchResult resultTwo = builder.build(); results.add(resultTwo); // Intent Three builder.addTitle(titleThree) .addSummary(summaryThree); SearchResult resultThree = builder.build(); results.add(resultThree); // Inline Two builder.addTitle(titleTwo) .addSummary(summaryTwo) .addPayload(inlinePayload); SearchResult resultFour = builder.build(); results.add(resultFour); // Intent Four builder.addTitle(titleFour) .addSummary(summaryOne) .addPayload(intentPayload); SearchResult resultFive = builder.build(); results.add(resultFive); loader = new DatabaseResultLoader(mContext, "", null); loader.removeDuplicates(results); assertThat(results.size()).isEqualTo(4); assertThat(results.get(0)).isEqualTo(resultOne); assertThat(results.get(1)).isEqualTo(resultThree); assertThat(results.get(2)).isEqualTo(resultFour); assertThat(results.get(3)).isEqualTo(resultFive); } @Test public void testSpecialCaseTwoWords_firstWordMatches_ranksHigher() { final String caseOne = "Apple pear"; final String caseTwo = "Banana apple"; insertSpecialCase(caseOne); Loading