From 12daf6830de241cc86408f904300ff0bdee02a9c Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 8 Dec 2016 15:40:05 -0800 Subject: [PATCH] Handle tap on intent based search results. Also fix how icon is loaded. IconResId is specific to the package of the indexed result. If result comes from external app, icon needs to be decoded against the external app's package context. Bug: 33432310 Test: RunSettingsRoboTests Change-Id: Ia0c53e63be757405dfaeceb2d865e7d8de87c5ee --- .../search2/DatabaseResultLoader.java | 125 +++++++++++++----- .../search2/IntentSearchViewHolder.java | 3 + .../settings/search2/ResultPayload.java | 11 ++ .../settings/search2/SearchResult.java | 12 +- .../search/DatabaseResultLoaderTest.java | 65 +++++++-- .../search/SearchResultBuilderTest.java | 8 +- 6 files changed, 166 insertions(+), 58 deletions(-) diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java index a4e614f3de2..b268f6a1b6e 100644 --- a/src/com/android/settings/search2/DatabaseResultLoader.java +++ b/src/com/android/settings/search2/DatabaseResultLoader.java @@ -16,32 +16,37 @@ package com.android.settings.search2; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; -import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import com.android.settings.search.Index; import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.utils.AsyncLoader; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; - -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; +import java.util.Map; /** * AsyncTask to retrieve Settings, First party app and any intent based results. */ public class DatabaseResultLoader extends AsyncLoader> { + private static final String LOG = "DatabaseResultLoader"; private final String mQueryText; private final Context mContext; protected final SQLiteDatabase mDatabase; @@ -65,7 +70,7 @@ public class DatabaseResultLoader extends AsyncLoader> { } String query = getSQLQuery(); - Cursor result = mDatabase.rawQuery(query, null); + Cursor result = mDatabase.rawQuery(query, null); return parseCursorForSearch(result); } @@ -78,10 +83,12 @@ public class DatabaseResultLoader extends AsyncLoader> { protected String getSQLQuery() { return String.format("SELECT data_rank, data_title, data_summary_on, " + - "data_summary_off, data_entries, data_keywords, class_name, screen_title, icon, " + - "intent_action, intent_target_package, intent_target_class, enabled, " + - "data_key_reference FROM prefs_index WHERE prefs_index MATCH 'data_title:%s* " + - "OR data_title_normalized:%s* OR data_keywords:%s*' AND locale = 'en_US'", + "data_summary_off, data_entries, data_keywords, class_name, screen_title," + + " icon, " + + "intent_action, intent_target_package, intent_target_class, enabled, " + + "data_key_reference FROM prefs_index WHERE prefs_index MATCH " + + "'data_title:%s* " + + "OR data_title_normalized:%s* OR data_keywords:%s*' AND locale = 'en_US'", mQueryText, mQueryText, mQueryText); } @@ -90,35 +97,89 @@ public class DatabaseResultLoader extends AsyncLoader> { if (cursorResults == null) { return null; } + final Map contextMap = new HashMap<>(); final ArrayList results = new ArrayList<>(); while (cursorResults.moveToNext()) { - final String title = cursorResults.getString(Index.COLUMN_INDEX_TITLE); - final String summaryOn = cursorResults.getString(COLUMN_INDEX_RAW_SUMMARY_ON); - final ArrayList breadcrumbs = new ArrayList<>(); - final int rank = cursorResults.getInt(COLUMN_INDEX_XML_RES_RANK); - - final String intentString = cursorResults.getString(Index.COLUMN_INDEX_INTENT_ACTION); - final IntentPayload intentPayload = new IntentPayload(new Intent(intentString)); - final int iconID = cursorResults.getInt(COLUMN_INDEX_RAW_ICON_RESID); - Drawable icon; - try { - icon = mContext.getDrawable(iconID); - } catch (Resources.NotFoundException nfe) { - icon = mContext.getDrawable(R.drawable.ic_search_history); + SearchResult result = buildSingleSearchResultFromCursor(contextMap, cursorResults); + if (result != null) { + results.add(result); } - - SearchResult.Builder builder = new SearchResult.Builder(); - builder.addTitle(title) - .addSummary(summaryOn) - .addBreadcrumbs(breadcrumbs) - .addRank(rank) - .addIcon(icon) - .addPayload(intentPayload); - results.add(builder.build()); } Collections.sort(results); return results; } + private SearchResult buildSingleSearchResultFromCursor(Map contextMap, + Cursor cursor) { + final String pkgName = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); + final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION); + final String title = cursor.getString(Index.COLUMN_INDEX_TITLE); + final String summaryOn = cursor.getString(Index.COLUMN_INDEX_SUMMARY_ON); + final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME); + final int rank = cursor.getInt(Index.COLUMN_INDEX_RANK); + final String key = cursor.getString(Index.COLUMN_INDEX_KEY); + final String iconResStr = cursor.getString(Index.COLUMN_INDEX_ICON); + + final ResultPayload payload; + if (TextUtils.isEmpty(action)) { + final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE); + // Action is null, we will launch it as a sub-setting + final Bundle args = new Bundle(); + args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); + final Intent intent = Utils.onBuildStartFragmentIntent(mContext, + className, args, null, 0, screenTitle, false); + payload = new IntentPayload(intent); + } else { + final Intent intent = new Intent(action); + final String targetClass = cursor.getString( + Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS); + if (!TextUtils.isEmpty(pkgName) && !TextUtils.isEmpty(targetClass)) { + final ComponentName component = new ComponentName(pkgName, targetClass); + intent.setComponent(component); + } + intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); + payload = new IntentPayload(intent); + } + SearchResult.Builder builder = new SearchResult.Builder(); + builder.addTitle(title) + .addSummary(summaryOn) + .addRank(rank) + .addIcon(getIconForPackage(contextMap, pkgName, className, iconResStr)) + .addPayload(payload); + return builder.build(); + } + + private Drawable getIconForPackage(Map contextMap, String pkgName, + String className, String iconResStr) { + final int iconId = TextUtils.isEmpty(iconResStr) + ? 0 : Integer.parseInt(iconResStr); + Drawable icon; + Context packageContext; + if (iconId == 0) { + icon = null; + } else { + if (TextUtils.isEmpty(className) && !TextUtils.isEmpty(pkgName)) { + packageContext = contextMap.get(pkgName); + if (packageContext == null) { + try { + packageContext = mContext.createPackageContext(pkgName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG, "Cannot create Context for package: " + pkgName); + return null; + } + contextMap.put(pkgName, packageContext); + } + } else { + packageContext = mContext; + } + try { + icon = packageContext.getDrawable(iconId); + } catch (Resources.NotFoundException nfe) { + icon = null; + } + } + return icon; + } + } diff --git a/src/com/android/settings/search2/IntentSearchViewHolder.java b/src/com/android/settings/search2/IntentSearchViewHolder.java index 0187c1c1dea..0ef27d01f7b 100644 --- a/src/com/android/settings/search2/IntentSearchViewHolder.java +++ b/src/com/android/settings/search2/IntentSearchViewHolder.java @@ -44,6 +44,9 @@ public class IntentSearchViewHolder extends SearchViewHolder { titleView.setText(result.title); summaryView.setText(result.summary); iconView.setImageDrawable(result.icon); + if (result.icon == null) { + iconView.setBackgroundResource(R.drawable.empty_icon); + } itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/src/com/android/settings/search2/ResultPayload.java b/src/com/android/settings/search2/ResultPayload.java index 3a4e47793d0..84df7b6f65b 100644 --- a/src/com/android/settings/search2/ResultPayload.java +++ b/src/com/android/settings/search2/ResultPayload.java @@ -31,8 +31,19 @@ public abstract class ResultPayload implements Parcelable { @IntDef({PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH, PayloadType.INTENT}) @Retention(RetentionPolicy.SOURCE) public @interface PayloadType { + /** + * Resulting page will be started using an intent + */ int INTENT = 0; + + /** + * Result is a inline widget, using a slider widget as UI. + */ int INLINE_SLIDER = 1; + + /** + * Result is a inline widget, using a toggle widget as UI. + */ int INLINE_SWITCH = 2; } diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java index 9fb250f5d48..5bf757f947f 100644 --- a/src/com/android/settings/search2/SearchResult.java +++ b/src/com/android/settings/search2/SearchResult.java @@ -83,7 +83,6 @@ public class SearchResult implements Comparable { payload = builder.mResultPayload; viewType = payload.getType(); stableId = Objects.hash(title, summary, breadcrumbs, rank, icon, payload, viewType); - } @Override @@ -98,7 +97,7 @@ public class SearchResult implements Comparable { protected CharSequence mTitle; protected CharSequence mSummary; protected ArrayList mBreadcrumbs; - protected int mRank = -1; + protected int mRank = 42; protected ResultPayload mResultPayload; protected Drawable mIcon; @@ -118,10 +117,9 @@ public class SearchResult implements Comparable { } public Builder addRank(int rank) { - if (rank < 0 || rank > 9) { - rank = 42; + if (rank >= 0 && rank <= 9) { + mRank = rank; } - mRank = rank; return this; } @@ -139,10 +137,6 @@ public class SearchResult implements Comparable { // Check that all of the mandatory fields are set. if (mTitle == null) { throw new IllegalArgumentException("SearchResult missing title argument"); - } else if (mRank == -1) { - throw new IllegalArgumentException("SearchResult missing rank argument"); - } else if (mIcon == null) { - throw new IllegalArgumentException("SearchResult missing icon argument"); } else if (mResultPayload == null) { throw new IllegalArgumentException("SearchResult missing Payload argument"); } diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java index a744bb7aea9..1df7b1f09ef 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java @@ -22,20 +22,23 @@ import android.content.Context; import android.content.Intent; import android.database.MatrixCursor; import android.graphics.drawable.Drawable; + +import com.android.settings.R; import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.SubSettings; import com.android.settings.TestConfig; +import com.android.settings.gestures.GestureSettings; import com.android.settings.search2.DatabaseResultLoader; import com.android.settings.search2.IntentPayload; import com.android.settings.search2.ResultPayload; import com.android.settings.search2.ResultPayload.PayloadType; import com.android.settings.search2.SearchResult; -import com.android.settings.R; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - -import org.robolectric.annotation.Config; import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.List; @@ -47,7 +50,11 @@ import static com.google.common.truth.Truth.assertThat; public class DatabaseResultLoaderTest { private DatabaseResultLoader mLoader; - private static final String[] TITLES = new String[] {"title1", "title2", "title3"}; + private static final String[] COLUMNS = new String[]{"rank", "title", "summary_on", + "summary off", "entries", "keywords", "class name", "screen title", "icon", + "intent action", "target package", "target class", "enabled", "key", "user id"}; + + private static final String[] TITLES = new String[]{"title1", "title2", "title3"}; private static final String SUMMARY = "SUMMARY"; private static final int EXAMPLES = 3; private static final Intent mIntent = new Intent("com.android.settings"); @@ -107,6 +114,16 @@ public class DatabaseResultLoaderTest { } } + @Test + public void testParseCursor_NoIcon() { + List results = mLoader.parseCursorForSearch( + getDummyCursor(false /* hasIcon */)); + for (int i = 0; i < EXAMPLES; i++) { + Drawable resultDrawable = results.get(i).icon; + assertThat(resultDrawable).isNull(); + } + } + @Test public void testParseCursor_MatchesPayloadType() { List results = mLoader.parseCursorForSearch(getDummyCursor()); @@ -117,6 +134,33 @@ public class DatabaseResultLoaderTest { } } + @Test + public void testParseCursor_MatchesIntentForSubSettings() { + MatrixCursor cursor = new MatrixCursor(COLUMNS); + final String BLANK = ""; + cursor.addRow(new Object[]{ + 0, // rank + TITLES[0], + SUMMARY, + SUMMARY, // summary off + BLANK, // entries + BLANK, // Keywords + GestureSettings.class.getName(), + BLANK, // screen title + null, // icon + BLANK, // action + null, // target package + BLANK, // target class + BLANK, // enabled + BLANK, // key + BLANK // user id + }); + List results = mLoader.parseCursorForSearch(cursor); + IntentPayload payload = (IntentPayload) results.get(0).payload; + Intent intent = payload.intent; + assertThat(intent.getComponent().getClassName()).isEqualTo(SubSettings.class.getName()); + } + @Test public void testParseCursor_MatchesIntentPayload() { List results = mLoader.parseCursorForSearch(getDummyCursor()); @@ -129,14 +173,15 @@ public class DatabaseResultLoaderTest { } private MatrixCursor getDummyCursor() { - String[] columns = new String[] {"rank", "title", "summary_on", "summary off", "entries", - "keywords", "class name", "screen title", "icon", "intent action", - "target package", "target class", "enabled", "key", "user id"}; - MatrixCursor cursor = new MatrixCursor(columns); + return getDummyCursor(true /* hasIcon */); + } + + private MatrixCursor getDummyCursor(boolean hasIcon) { + MatrixCursor cursor = new MatrixCursor(COLUMNS); final String BLANK = ""; for (int i = 0; i < EXAMPLES; i++) { - ArrayList item = new ArrayList<>(columns.length); + ArrayList item = new ArrayList<>(COLUMNS.length); item.add(Integer.toString(i)); item.add(TITLES[i]); item.add(SUMMARY); @@ -145,7 +190,7 @@ public class DatabaseResultLoaderTest { item.add(BLANK); // keywords item.add(BLANK); // classname item.add(BLANK); // screen title - item.add(Integer.toString(mIcon)); + item.add(hasIcon ? Integer.toString(mIcon) : null); item.add(mIntent.getAction()); item.add(BLANK); // target package item.add(BLANK); // target class diff --git a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java index a0f4cc52c48..5649a95e5cd 100644 --- a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java @@ -124,13 +124,7 @@ public class SearchResultBuilderTest { .addBreadcrumbs(mBreadcrumbs) .addPayload(mResultPayload); - SearchResult result = null; - try { - result = mBuilder.build(); - } catch (IllegalArgumentException e) { - // passes. - } - assertThat(result).isNull(); + assertThat(mBuilder.build()).isNotNull(); } @Test