From 0ed37c351333429f58e1561fcb8e6af9c2041507 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Mon, 24 Oct 2016 10:12:49 -0700 Subject: [PATCH] Fork Search code to independantly develop and test search. This is the start of the new search in Settings. It is a nearly complete replacement of the old search code in a more modular and flexible architecture. It is expanding the datasources that it queries, including the same Settings database, which will now include more first party apps and be extended to support inline results where the user can change settings directly from the search view. Search will also fan out to query new sources (local or remote), and is built in a way such that adding additional sources is roughly the same amount of work had they been added in the initial writing of this code. Query interpretation will now be source-dependant, allowing for future upgrades to fuzzy search where it is applicable. Change-Id: Ib0bac1fe92bf8a662d33abf9a99bb6ee2090ec8f Fixes: 32115225, 32378927 Test: make RunSettingsRoboTests --- AndroidManifest.xml | 5 + res/layout/search_intent_item.xml | 63 ++++++ res/layout/search_main.xml | 24 +++ res/layout/search_panel_2.xml | 62 ++++++ res/menu/search_options_menu.xml | 23 +++ .../android/settings/SettingsActivity.java | 34 +++- .../settings/overlay/FeatureFactory.java | 3 + .../settings/overlay/FeatureFactoryImpl.java | 13 +- .../search2/DatabaseResultLoader.java | 124 ++++++++++++ .../settings/search2/InlineSliderPayload.java | 62 ++++++ .../settings/search2/IntentPayload.java | 63 ++++++ .../search2/IntentSearchViewHolder.java | 44 ++++ .../settings/search2/ResultPayload.java | 40 ++++ .../settings/search2/SearchActivity.java | 41 ++++ .../search2/SearchFeatureProvider.java | 38 ++++ .../search2/SearchFeatureProviderImpl.java | 63 ++++++ .../settings/search2/SearchFragment.java | 158 +++++++++++++++ .../settings/search2/SearchResult.java | 146 ++++++++++++++ .../search2/SearchResultsAdapter.java | 101 ++++++++++ .../settings/search2/SearchViewHolder.java | 33 +++ .../SettingsRobolectricTestRunner.java | 4 + .../search/DatabaseResultLoaderTest.java | 160 +++++++++++++++ .../search/InlineSliderPayloadTest.java | 52 +++++ .../settings/search/IntentPayloadTest.java | 56 ++++++ .../search/IntentSearchViewHolderTest.java | 89 +++++++++ .../settings/search/SearchAdapterTest.java | 104 ++++++++++ .../search/SearchFeatureProviderImplTest.java | 68 +++++++ .../search/SearchResultBuilderTest.java | 188 ++++++++++++++++++ .../testutils/FakeFeatureFactory.java | 8 + 29 files changed, 1861 insertions(+), 8 deletions(-) create mode 100644 res/layout/search_intent_item.xml create mode 100644 res/layout/search_main.xml create mode 100644 res/layout/search_panel_2.xml create mode 100644 res/menu/search_options_menu.xml create mode 100644 src/com/android/settings/search2/DatabaseResultLoader.java create mode 100644 src/com/android/settings/search2/InlineSliderPayload.java create mode 100644 src/com/android/settings/search2/IntentPayload.java create mode 100644 src/com/android/settings/search2/IntentSearchViewHolder.java create mode 100644 src/com/android/settings/search2/ResultPayload.java create mode 100644 src/com/android/settings/search2/SearchActivity.java create mode 100644 src/com/android/settings/search2/SearchFeatureProvider.java create mode 100644 src/com/android/settings/search2/SearchFeatureProviderImpl.java create mode 100644 src/com/android/settings/search2/SearchFragment.java create mode 100644 src/com/android/settings/search2/SearchResult.java create mode 100644 src/com/android/settings/search2/SearchResultsAdapter.java create mode 100644 src/com/android/settings/search2/SearchViewHolder.java create mode 100644 tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java create mode 100644 tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java create mode 100644 tests/robotests/src/com/android/settings/search/IntentPayloadTest.java create mode 100644 tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java create mode 100644 tests/robotests/src/com/android/settings/search/SearchAdapterTest.java create mode 100644 tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java create mode 100644 tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4fb17f7172e..504f12f482c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -216,6 +216,11 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/search_main.xml b/res/layout/search_main.xml new file mode 100644 index 00000000000..ab728cd1720 --- /dev/null +++ b/res/layout/search_main.xml @@ -0,0 +1,24 @@ + + + + diff --git a/res/layout/search_panel_2.xml b/res/layout/search_panel_2.xml new file mode 100644 index 00000000000..671c19c0bdc --- /dev/null +++ b/res/layout/search_panel_2.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/menu/search_options_menu.xml b/res/menu/search_options_menu.xml new file mode 100644 index 00000000000..25a79d4efe8 --- /dev/null +++ b/res/menu/search_options_menu.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index f67f73f712f..1831755b0c5 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -138,6 +138,8 @@ import com.android.settings.print.PrintSettingsFragment; import com.android.settings.qstile.DevelopmentTiles; import com.android.settings.search.DynamicIndexableContentMonitor; import com.android.settings.search.Index; +import com.android.settings.search2.SearchFeatureProvider; +import com.android.settings.search2.SearchFragment; import com.android.settings.sim.SimSettings; import com.android.settings.system.SystemDashboardFragment; import com.android.settings.tts.TextToSpeechSettings; @@ -479,6 +481,8 @@ public class SettingsActivity extends SettingsDrawerActivity private SearchResultsSummary mSearchResultsFragment; private String mSearchQuery; + private SearchFeatureProvider mSearchFeatureProvider; + // Categories private ArrayList mCategories = new ArrayList(); @@ -528,9 +532,14 @@ public class SettingsActivity extends SettingsDrawerActivity } MenuInflater inflater = getMenuInflater(); + if (mSearchFeatureProvider.isEnabled()) { + mSearchFeatureProvider.setUpSearchMenu(menu, this); + return true; + } inflater.inflate(R.menu.options_menu, menu); - // Cache the search query (can be overriden by the OnQueryTextListener) + + // Cache the search query (can be overridden by the OnQueryTextListener) final String query = mSearchQuery; mSearchMenuItem = menu.findItem(R.id.search); @@ -553,7 +562,6 @@ public class SettingsActivity extends SettingsDrawerActivity mSearchMenuItem.expandActionView(); } mSearchView.setQuery(query, true /* submit */); - return true; } @@ -596,8 +604,12 @@ public class SettingsActivity extends SettingsDrawerActivity protected void onCreate(Bundle savedState) { super.onCreate(savedState); long startTime = System.currentTimeMillis(); - mDashboardFeatureProvider = - FeatureFactory.getFactory(this).getDashboardFeatureProvider(this); + + final FeatureFactory factory = FeatureFactory.getFactory(this); + + mDashboardFeatureProvider = factory.getDashboardFeatureProvider(this); + mSearchFeatureProvider = factory.getSearchFeatureProvider(this); + // Should happen before any call to getIntent() getMetaData(); @@ -1274,19 +1286,24 @@ public class SettingsActivity extends SettingsDrawerActivity return super.shouldUpRecreateTask(new Intent(this, SettingsActivity.class)); } + @Deprecated @Override public boolean onQueryTextSubmit(String query) { - switchToSearchResultsFragmentIfNeeded(); + if (mSearchFeatureProvider.isEnabled()) { + return false; + } mSearchQuery = query; + switchToSearchResultsFragmentIfNeeded(); return mSearchResultsFragment.onQueryTextSubmit(query); } + @Deprecated @Override public boolean onQueryTextChange(String newText) { - mSearchQuery = newText; - if (mSearchResultsFragment == null) { + if (mSearchFeatureProvider.isEnabled() || mSearchResultsFragment == null) { return false; } + mSearchQuery = newText; return mSearchResultsFragment.onQueryTextChange(newText); } @@ -1330,6 +1347,7 @@ public class SettingsActivity extends SettingsDrawerActivity } } + @Deprecated private void switchToSearchResultsFragmentIfNeeded() { if (mSearchResultsFragment != null) { return; @@ -1347,10 +1365,12 @@ public class SettingsActivity extends SettingsDrawerActivity mSearchMenuItemExpanded = true; } + @Deprecated public void needToRevertToInitialFragment() { mNeedToRevertToInitialFragment = true; } + @Deprecated private void revertToInitialFragment() { mNeedToRevertToInitialFragment = false; mSearchResultsFragment = null; diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java index 55ea4bb1ab1..75f10011b55 100644 --- a/src/com/android/settings/overlay/FeatureFactory.java +++ b/src/com/android/settings/overlay/FeatureFactory.java @@ -27,6 +27,7 @@ import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; +import com.android.settings.search2.SearchFeatureProvider; /** * Abstract class for creating feature controllers. Allows OEM implementations to define their own @@ -80,6 +81,8 @@ public abstract class FeatureFactory { public abstract EnterprisePrivacyFeatureProvider getEnterprisePrivacyFeatureProvider( Context context); + public abstract SearchFeatureProvider getSearchFeatureProvider(Context context); + public static final class FactoryNotFoundException extends RuntimeException { public FactoryNotFoundException(Throwable throwable) { super("Unable to create factory. Did you misconfigure Proguard?", throwable); diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java index ec0ff463ce5..4a7396e9316 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.java +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java @@ -32,6 +32,8 @@ import com.android.settings.enterprise.EnterprisePrivacyFeatureProviderImpl; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProviderImpl; +import com.android.settings.search2.SearchFeatureProvider; +import com.android.settings.search2.SearchFeatureProviderImpl; /** * {@link FeatureFactory} implementation for AOSP Settings. @@ -44,6 +46,7 @@ public final class FeatureFactoryImpl extends FeatureFactory { private DashboardFeatureProviderImpl mDashboardFeatureProvider; private LocaleFeatureProvider mLocaleFeatureProvider; private EnterprisePrivacyFeatureProvider mEnterprisePrivacyFeatureProvider; + private SearchFeatureProvider mSearchFeatureProvider; @Override public SupportFeatureProvider getSupportFeatureProvider(Context context) { @@ -91,9 +94,17 @@ public final class FeatureFactoryImpl extends FeatureFactory { public EnterprisePrivacyFeatureProvider getEnterprisePrivacyFeatureProvider(Context context) { if (mEnterprisePrivacyFeatureProvider == null) { mEnterprisePrivacyFeatureProvider = new EnterprisePrivacyFeatureProviderImpl(context, - new DevicePolicyManagerWrapperImpl((DevicePolicyManager)context + new DevicePolicyManagerWrapperImpl((DevicePolicyManager) context .getSystemService(Context.DEVICE_POLICY_SERVICE))); } return mEnterprisePrivacyFeatureProvider; } + + @Override + public SearchFeatureProvider getSearchFeatureProvider(Context context) { + if (mSearchFeatureProvider == null) { + mSearchFeatureProvider = new SearchFeatureProviderImpl(context); + } + return mSearchFeatureProvider; + } } diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java new file mode 100644 index 00000000000..aca94b1a471 --- /dev/null +++ b/src/com/android/settings/search2/DatabaseResultLoader.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.drawable.Drawable; +import android.support.annotation.VisibleForTesting; +import com.android.settings.search.Index; +import com.android.settings.search.IndexDatabaseHelper; +import com.android.settings.utils.AsyncLoader; +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.Collections; +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; + + +/** + * AsyncTask to retrieve Settings, First party app and any intent based results. + */ +public class DatabaseResultLoader extends AsyncLoader> { + private final String mQueryText; + private final Context mContext; + protected final SQLiteDatabase mDatabase; + + public DatabaseResultLoader(Context context, String queryText) { + super(context); + mDatabase = IndexDatabaseHelper.getInstance(context).getReadableDatabase(); + mQueryText = queryText; + mContext = context; + } + + @Override + protected void onDiscardResult(List result) { + // TODO Search + } + + @Override + public List loadInBackground() { + if (mQueryText == null || mQueryText.isEmpty()) { + return null; + } + + String query = getSQLQuery(); + Cursor result = mDatabase.rawQuery(query, null); + + return parseCursorForSearch(result); + } + + @Override + protected boolean onCancelLoad() { + // TODO + return super.onCancelLoad(); + } + + 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'", + mQueryText, mQueryText, mQueryText); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public ArrayList parseCursorForSearch(Cursor cursorResults) { + if (cursorResults == null) { + return null; + } + 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.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; + } + +} diff --git a/src/com/android/settings/search2/InlineSliderPayload.java b/src/com/android/settings/search2/InlineSliderPayload.java new file mode 100644 index 00000000000..8f08d5992bf --- /dev/null +++ b/src/com/android/settings/search2/InlineSliderPayload.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.net.Uri; +import android.os.Parcel; + +/** + * Payload for Inline Settings results represented by a Slider. + */ +public class InlineSliderPayload extends ResultPayload { + public final Uri uri; + + private InlineSliderPayload(Parcel in) { + uri = in.readParcelable(InlineSliderPayload.class.getClassLoader()); + } + + public InlineSliderPayload(Uri newUri) { + uri = newUri; + } + + @Override + public int getType() { + return PayloadType.INLINE_SLIDER; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(uri, flags); + } + + public static final Creator CREATOR = new Creator() { + @Override + public InlineSliderPayload createFromParcel(Parcel in) { + return new InlineSliderPayload(in); + } + + @Override + public InlineSliderPayload[] newArray(int size) { + return new InlineSliderPayload[size]; + } + }; +} \ No newline at end of file diff --git a/src/com/android/settings/search2/IntentPayload.java b/src/com/android/settings/search2/IntentPayload.java new file mode 100644 index 00000000000..1ef37973987 --- /dev/null +++ b/src/com/android/settings/search2/IntentPayload.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.content.Intent; +import android.os.Parcel; +import com.android.internal.annotations.VisibleForTesting; + +/** + * Encapsulates the standard intent based results as seen in first party apps and Settings results. + */ +public class IntentPayload extends ResultPayload { + public final Intent intent; + + private IntentPayload(Parcel in) { + intent = in.readParcelable(IntentPayload.class.getClassLoader()); + } + + public IntentPayload(Intent newIntent) { + intent = newIntent; + } + + @ResultPayload.PayloadType public int getType() { + return PayloadType.INTENT; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(intent, flags); + } + + public static final Creator CREATOR = new Creator() { + @Override + public IntentPayload createFromParcel(Parcel in) { + return new IntentPayload(in); + } + + @Override + public IntentPayload[] newArray(int size) { + return new IntentPayload[size]; + } + }; + +} \ No newline at end of file diff --git a/src/com/android/settings/search2/IntentSearchViewHolder.java b/src/com/android/settings/search2/IntentSearchViewHolder.java new file mode 100644 index 00000000000..0b99d6e58d5 --- /dev/null +++ b/src/com/android/settings/search2/IntentSearchViewHolder.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.settings.R; + +/** + * ViewHolder for intent based search results. + * The DatabaseResultLoader is the primary use case for this ViewHolder. + */ +public class IntentSearchViewHolder extends SearchViewHolder { + public final TextView titleView; + public final TextView summaryView; + public final ImageView iconView; + + public IntentSearchViewHolder(View view) { + super(view); + titleView = (TextView) view.findViewById(R.id.title); + summaryView = (TextView) view.findViewById(R.id.summary); + iconView= (ImageView) view.findViewById(R.id.icon); + } + + public void onBind(SearchResult result) { + titleView.setText(result.title); + summaryView.setText(result.summary); + iconView.setImageDrawable(result.icon); + } +} diff --git a/src/com/android/settings/search2/ResultPayload.java b/src/com/android/settings/search2/ResultPayload.java new file mode 100644 index 00000000000..3a4e47793d0 --- /dev/null +++ b/src/com/android/settings/search2/ResultPayload.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.annotation.IntDef; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A interface for search results types. Examples include Inline results, third party apps + * or any future possibilities. + */ +public abstract class ResultPayload implements Parcelable { + + @IntDef({PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH, PayloadType.INTENT}) + @Retention(RetentionPolicy.SOURCE) + public @interface PayloadType { + int INTENT = 0; + int INLINE_SLIDER = 1; + int INLINE_SWITCH = 2; + } + + @ResultPayload.PayloadType public abstract int getType(); +} diff --git a/src/com/android/settings/search2/SearchActivity.java b/src/com/android/settings/search2/SearchActivity.java new file mode 100644 index 00000000000..25a54cfcf47 --- /dev/null +++ b/src/com/android/settings/search2/SearchActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.os.Bundle; + +import com.android.settings.R; + +public class SearchActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.search_main); + + FragmentManager fragmentManager = getFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.main_content); + if (fragment == null) { + fragmentManager.beginTransaction() + .add(R.id.main_content, new SearchFragment()) + .commit(); + } + } +} diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java new file mode 100644 index 00000000000..14f5d134ecb --- /dev/null +++ b/src/com/android/settings/search2/SearchFeatureProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.app.Activity; +import android.widget.SearchView; +import android.view.Menu; + +/** + * FeatureProvider for Settings Search + */ +public interface SearchFeatureProvider { + + /** + * @return true to use the new version of search + */ + boolean isEnabled(); + + /** + * Inserts the Menu items into Settings activity. + * @param menu Items will be inserted into this menu. + * @param activity The activity that precedes SearchActivity. + */ + void setUpSearchMenu(Menu menu, Activity activity); +} diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java new file mode 100644 index 00000000000..3c6dc3501ea --- /dev/null +++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.widget.SearchView; +import android.view.Menu; + +import android.view.MenuItem; +import com.android.settings.R; + +/** + * FeatureProvider for the refactored search code. + */ +public class SearchFeatureProviderImpl implements SearchFeatureProvider { + protected Context mContext; + + + public SearchFeatureProviderImpl(Context context) { + mContext = context; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void setUpSearchMenu(Menu menu, final Activity activity) { + if (menu == null || activity == null) { + return; + } + String menuTitle = mContext.getString(R.string.search_menu); + MenuItem menuItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, menuTitle) + .setIcon(R.drawable.abc_ic_search_api_material) + .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + Intent intent = new Intent(activity, SearchActivity.class); + activity.startActivity(intent); + return true; + } + }); + + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + } +} diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java new file mode 100644 index 00000000000..18f20bef60d --- /dev/null +++ b/src/com/android/settings/search2/SearchFragment.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.app.Activity; +import android.content.Loader; +import android.os.Bundle; +import android.app.LoaderManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Menu; +import android.view.MenuInflater; +import android.widget.SearchView; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; + +import java.util.List; + +public class SearchFragment extends InstrumentedFragment implements + SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener, + LoaderManager.LoaderCallbacks> { + + private static final int DATABASE_LOADER_ID = 0; + + private SearchResultsAdapter mSearchAdapter; + + private DatabaseResultLoader mSearchLoader; + + private RecyclerView mResultsRecyclerView; + private SearchView mSearchView; + private MenuItem mSearchMenuItem; + + private String mQuery; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + mSearchAdapter = new SearchResultsAdapter(); + + final LoaderManager loaderManager = getLoaderManager(); + loaderManager.initLoader(DATABASE_LOADER_ID, null, this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.search_panel_2, container, false); + mResultsRecyclerView = (RecyclerView) view.findViewById(R.id.list_results); + + mResultsRecyclerView.setAdapter(mSearchAdapter); + mResultsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + return view; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.search_options_menu, menu); + + + mSearchMenuItem = menu.findItem(R.id.search); + + mSearchView = (SearchView) mSearchMenuItem.getActionView(); + mSearchView.setOnQueryTextListener(this); + mSearchView.setMaxWidth(Integer.MAX_VALUE); + mSearchMenuItem.expandActionView(); + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + // Return false to prevent the search box from collapsing. + return false; + } + + @Override + public boolean onQueryTextChange(String query) { + if (query == null || query.equals(mQuery)) { + return false; + } + + mQuery = query; + clearLoaders(); + + final LoaderManager loaderManager = getLoaderManager(); + loaderManager.restartLoader(DATABASE_LOADER_ID, null, this); + + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + final Activity activity = getActivity(); + + switch (id) { + case DATABASE_LOADER_ID: + mSearchLoader = new DatabaseResultLoader(activity, mQuery); + return mSearchLoader; + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + if (data == null) { + return; + } + + mSearchAdapter.mergeResults(data, loader.getClass().getName()); + } + + @Override + public void onLoaderReset(Loader> loader) { } + + @Override + public int getMetricsCategory() { + return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS; + } + + private void clearLoaders() { + if (mSearchLoader != null) { + mSearchLoader.cancelLoad(); + mSearchLoader = null; + } + } +} diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java new file mode 100644 index 00000000000..e483df3021a --- /dev/null +++ b/src/com/android/settings/search2/SearchResult.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; + +/** + * Dataclass as an interface for all Search Results. + */ +public class SearchResult implements Comparable { + @Override + public int compareTo(SearchResult searchResult) { + if (searchResult == null) { + return -1; + } + return this.rank - searchResult.rank; + } + + public static class Builder { + protected String mTitle; + protected String mSummary; + protected ArrayList mBreadcrumbs; + protected int mRank = -1; + protected ResultPayload mResultPayload; + protected Drawable mIcon; + + public Builder addTitle(String title) { + mTitle = title; + return this; + } + + public Builder addSummary(String summary) { + mSummary = summary; + return this; + } + + public Builder addBreadcrumbs(ArrayList breadcrumbs) { + mBreadcrumbs = breadcrumbs; + return this; + } + + public Builder addRank(int rank) { + if (rank < 0 || rank > 9) { + rank = 42; + } + mRank = rank; + return this; + } + + public Builder addIcon(Drawable icon) { + mIcon = icon; + return this; + } + + public Builder addPayload(ResultPayload payload) { + mResultPayload = payload; + return this; + } + + public SearchResult build() { + // Check that all of the mandatory fields are set. + if (mTitle == null) { + throw new IllegalArgumentException("SearchResult missing title argument"); + } else if (mSummary == null ) { + throw new IllegalArgumentException("SearchResult missing summary argument"); + } else if (mBreadcrumbs == null){ + throw new IllegalArgumentException("SearchResult missing breadcrumbs 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"); + } + return new SearchResult(this); + } + } + + /** + * The title of the result and main text displayed. + * Intent Results: Displays as the primary + */ + public final String title; + + /** + * Summary / subtitle text + * Intent Results: Displays the text underneath the title + */ + final public String summary; + + /** + * An ordered list of the information hierarchy. + * Intent Results: Displayed a hierarchy of selections to reach the setting from the home screen + */ + public final ArrayList breadcrumbs; + + /** + * A suggestion for the ranking of the result. + * Based on Settings Rank: + * 1 is a near perfect match + * 9 is the weakest match + * TODO subject to change + */ + public final int rank; + + /** + * Identifier for the recycler view adapter. + */ + @ResultPayload.PayloadType public final int viewType; + + /** + * Metadata for the specific result types. + */ + public final ResultPayload payload; + + /** + * Result's icon. + */ + public final Drawable icon; + + private SearchResult(Builder builder) { + title = builder.mTitle; + summary = builder.mSummary; + breadcrumbs = builder.mBreadcrumbs; + rank = builder.mRank; + icon = builder.mIcon; + payload = builder.mResultPayload; + viewType = payload.getType(); + } +} diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java new file mode 100644 index 00000000000..22f106b49bd --- /dev/null +++ b/src/com/android/settings/search2/SearchResultsAdapter.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.RecyclerView.Adapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.settings.R; +import com.android.settings.search2.ResultPayload.PayloadType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class SearchResultsAdapter extends Adapter { + private ArrayList mSearchResults; + private HashMap> mResultsMap; + + public SearchResultsAdapter() { + mSearchResults = new ArrayList<>(); + mResultsMap = new HashMap<>(); + + setHasStableIds(true); + } + + public void mergeResults(List freshResults, String loaderClassName) { + if (freshResults == null) { + return; + } + mResultsMap.put(loaderClassName, freshResults); + mSearchResults = mergeMappedResults(); + notifyDataSetChanged(); + } + + private ArrayList mergeMappedResults() { + ArrayList mergedResults = new ArrayList<>(); + for(String key : mResultsMap.keySet()) { + mergedResults.addAll(mResultsMap.get(key)); + } + return mergedResults; + } + + @Override + public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch(viewType) { + case PayloadType.INTENT: + View view = inflater.inflate(R.layout.search_intent_item, parent, false); + return new IntentSearchViewHolder(view); + case PayloadType.INLINE_SLIDER: + return null; + case PayloadType.INLINE_SWITCH: + return null; + default: + return null; + } + } + + @Override + public void onBindViewHolder(SearchViewHolder holder, int position) { + SearchResult result = mSearchResults.get(position); + holder.onBind(result); + } + + @Override + public long getItemId(int position) { + return super.getItemId(position); + } + + @Override + public int getItemViewType(int position) { + return mSearchResults.get(position).viewType; + } + + @Override + public int getItemCount() { + return mSearchResults.size(); + } + + @VisibleForTesting + public ArrayList getSearchResults() { + return mSearchResults; + } +} diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java new file mode 100644 index 00000000000..2f500fb7ede --- /dev/null +++ b/src/com/android/settings/search2/SearchViewHolder.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * The ViewHolder for the Search RecyclerView. + * There are multiple search result types in the same Recycler view with different UI requirements. + * Some examples include Intent results, Inline results, and Help articles. + */ +public abstract class SearchViewHolder extends RecyclerView.ViewHolder { + + public SearchViewHolder(View view) { + super(view); + } + + public abstract void onBind(SearchResult result); +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java index 1a7647d3962..d6d69632563 100644 --- a/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java +++ b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java @@ -64,6 +64,10 @@ public class SettingsRobolectricTestRunner extends RobolectricTestRunner { getPackageName(), Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"), null)); + paths.add(new ResourcePath( + getPackageName(), + Fs.fileFromPath("./frameworks/base/core/res/res"), + null)); return paths; } }; diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java new file mode 100644 index 00000000000..a744bb7aea9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 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.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.MatrixCursor; +import android.graphics.drawable.Drawable; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +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 java.util.ArrayList; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class DatabaseResultLoaderTest { + private DatabaseResultLoader mLoader; + + 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"); + private static final int mIcon = R.drawable.ic_search_history; + + private Drawable mDrawable; + + @Before + public void setUp() { + Context context = Robolectric.buildActivity(Activity.class).get(); + mDrawable = context.getDrawable(mIcon); + mLoader = new DatabaseResultLoader(context, ""); + } + + @Test + public void testParseNullResults_ReturnsNull() { + List results = mLoader.parseCursorForSearch(null); + assertThat(results).isNull(); + } + + @Test + public void testParseCursor_NotNull() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + assertThat(results).isNotNull(); + } + + @Test + public void testParseCursor_MatchesRank() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + for (int i = 0; i < EXAMPLES; i++) { + assertThat(results.get(i).rank).isEqualTo(i); + } + } + + @Test + public void testParseCursor_MatchesTitle() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + for (int i = 0; i < EXAMPLES; i++) { + assertThat(results.get(i).title).isEqualTo(TITLES[i]); + } + } + + @Test + public void testParseCursor_MatchesSummary() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + for (int i = 0; i < EXAMPLES; i++) { + assertThat(results.get(i).summary).isEqualTo(SUMMARY); + } + } + + @Test + public void testParseCursor_MatchesIcon() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + for (int i = 0; i < EXAMPLES; i++) { + Drawable resultDrawable = results.get(i).icon; + assertThat(resultDrawable.toString()).isEqualTo(mDrawable.toString()); + } + } + + @Test + public void testParseCursor_MatchesPayloadType() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + ResultPayload payload; + for (int i = 0; i < EXAMPLES; i++) { + payload = results.get(i).payload; + assertThat(payload.getType()).isEqualTo(PayloadType.INTENT); + } + } + + @Test + public void testParseCursor_MatchesIntentPayload() { + List results = mLoader.parseCursorForSearch(getDummyCursor()); + IntentPayload payload; + for (int i = 0; i < EXAMPLES; i++) { + payload = (IntentPayload) results.get(i).payload; + Intent intent = payload.intent; + assertThat(intent.getAction()).isEqualTo(mIntent.getAction()); + } + } + + 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); + final String BLANK = ""; + + for (int i = 0; i < EXAMPLES; i++) { + ArrayList item = new ArrayList<>(columns.length); + item.add(Integer.toString(i)); + item.add(TITLES[i]); + item.add(SUMMARY); + item.add(BLANK); // summary off + item.add(BLANK); // entries + item.add(BLANK); // keywords + item.add(BLANK); // classname + item.add(BLANK); // screen title + item.add(Integer.toString(mIcon)); + item.add(mIntent.getAction()); + item.add(BLANK); // target package + item.add(BLANK); // target class + item.add(BLANK); // enabled + item.add(BLANK); // key + item.add(BLANK); // user id + + cursor.addRow(item); + } + return cursor; + } +} diff --git a/tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java b/tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java new file mode 100644 index 00000000000..d52eb94653b --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 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.net.Uri; +import android.os.Parcel; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.search2.InlineSliderPayload; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class InlineSliderPayloadTest { + private InlineSliderPayload mPayload; + + @Test + public void testParcelOrdering_StaysValid() { + Uri uri = Uri.parse("http://www.TESTURI.com"); + Parcel parcel = Parcel.obtain(); + + mPayload = new InlineSliderPayload(uri); + mPayload.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + InlineSliderPayload newPayload = InlineSliderPayload.CREATOR.createFromParcel(parcel); + + String originalUri = mPayload.uri.toString(); + String copiedUri = newPayload.uri.toString(); + assertThat(originalUri).isEqualTo(copiedUri); + } +} diff --git a/tests/robotests/src/com/android/settings/search/IntentPayloadTest.java b/tests/robotests/src/com/android/settings/search/IntentPayloadTest.java new file mode 100644 index 00000000000..6f42622e676 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/IntentPayloadTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 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.Intent; +import android.os.Parcel; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.search2.IntentPayload; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class IntentPayloadTest { + private IntentPayload mPayload; + + private final String EXTRA_KEY = "key"; + private final String EXTRA_VALUE = "value"; + + @Test + public void testParcelOrdering_StaysValid() { + Intent intent = new Intent(); + intent.putExtra(EXTRA_KEY, EXTRA_VALUE); + Parcel parcel = Parcel.obtain(); + + mPayload = new IntentPayload(intent); + mPayload.writeToParcel(parcel, 0); + // Reset parcel for reading + parcel.setDataPosition(0); + IntentPayload newPayload = IntentPayload.CREATOR.createFromParcel(parcel); + + String originalIntentExtra = mPayload.intent.getStringExtra(EXTRA_KEY); + String copiedIntentExtra = newPayload.intent.getStringExtra(EXTRA_KEY); + assertThat(originalIntentExtra).isEqualTo(copiedIntentExtra); + } +} diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java new file mode 100644 index 00000000000..2534c0b2b9f --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 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.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import com.android.settings.R; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.search2.IntentPayload; +import com.android.settings.search2.IntentSearchViewHolder; +import com.android.settings.search2.SearchResult.Builder; +import com.android.settings.search2.SearchResult; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +import java.util.ArrayList; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class IntentSearchViewHolderTest { + private IntentSearchViewHolder mHolder; + private static Drawable mIcon; + + private static final String TITLE = "title"; + private static final String SUMMARY = "summary"; + + + @Before + public void setUp() { + final Context context = ShadowApplication.getInstance().getApplicationContext(); + View view = LayoutInflater.from(context).inflate(R.layout.search_intent_item, null); + mHolder = new IntentSearchViewHolder(view); + + mIcon = context.getDrawable(R.drawable.ic_search_history); + } + + @Test + public void testConstructor_MembersNotNull() { + assertThat(mHolder.titleView).isNotNull(); + assertThat(mHolder.summaryView).isNotNull(); + assertThat(mHolder.iconView).isNotNull(); + } + + @Test + public void testBindViewElements_AllUpdated() { + SearchResult result = getSearchResult(); + mHolder.onBind(result); + + assertThat(mHolder.titleView.getText()).isEqualTo(TITLE); + assertThat(mHolder.summaryView.getText()).isEqualTo(SUMMARY); + assertThat(mHolder.iconView.getDrawable()).isEqualTo(mIcon); + } + + private SearchResult getSearchResult() { + Builder builder = new Builder(); + builder.addTitle(TITLE) + .addSummary(SUMMARY) + .addRank(1) + .addPayload(new IntentPayload(null)) + .addBreadcrumbs(new ArrayList()) + .addIcon(mIcon); + + return builder.build(); + } +} diff --git a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java new file mode 100644 index 00000000000..b3da4eb0f62 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 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.app.Activity; +import android.content.Context; +import android.graphics.drawable.Drawable; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.search2.*; +import com.android.settings.search2.SearchResult.Builder; +import com.android.settings.R; + +import java.util.ArrayList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.annotation.Config; +import org.robolectric.Robolectric; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SearchAdapterTest { + + private SearchResultsAdapter mAdapter; + private Context mContext; + private String mLoaderClassName; + + @Before + public void setUp() { + mContext = Robolectric.buildActivity(Activity.class).get(); + mAdapter = new SearchResultsAdapter(); + mLoaderClassName = DatabaseResultLoader.class.getName(); + } + + private ArrayList getIntentSampleResults() { + ArrayList sampleResults = new ArrayList<>(); + ArrayList breadcrumbs = new ArrayList<>(); + final Drawable icon = mContext.getDrawable(R.drawable.ic_search_history); + final ResultPayload payload = new IntentPayload(null); + + SearchResult.Builder builder = new Builder(); + builder.addTitle("title") + .addSummary("summary") + .addRank(1) + .addBreadcrumbs(breadcrumbs) + .addIcon(icon) + .addPayload(payload); + sampleResults.add(builder.build()); + + builder.addRank(2); + sampleResults.add(builder.build()); + + builder.addRank(3); + sampleResults.add(builder.build()); + return sampleResults; + } + + + @Test + public void testNoResultsAdded_EmptyListReturned() { + ArrayList updatedResults = mAdapter.getSearchResults(); + assertThat(updatedResults).isEmpty(); + } + + + @Test + public void testSingleSourceMerge_ExactCopyReturned() { + ArrayList intentResults = getIntentSampleResults(); + mAdapter.mergeResults(intentResults, mLoaderClassName); + + ArrayList updatedResults = mAdapter.getSearchResults(); + assertThat(updatedResults).containsAllIn(intentResults); + } + + @Test + public void testDuplicateSourceMerge_ExactCopyReturned() { + ArrayList intentResults = getIntentSampleResults(); + mAdapter.mergeResults(intentResults, mLoaderClassName); + mAdapter.mergeResults(intentResults, mLoaderClassName); + + ArrayList updatedResults = mAdapter.getSearchResults(); + assertThat(updatedResults).containsAllIn(intentResults); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java new file mode 100644 index 00000000000..06f43221aeb --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016 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.app.Activity; +import android.view.Menu; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search2.SearchFeatureProviderImpl; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; + +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.verify; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SearchFeatureProviderImplTest { + private SearchFeatureProviderImpl mProvider; + private Activity mActivity; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Menu menu; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActivity = Robolectric.buildActivity(Activity.class).create().visible().get(); + mProvider = (SearchFeatureProviderImpl) FeatureFactory.getFactory(mActivity) + .getSearchFeatureProvider(mActivity); + } + + @Test + public void testPassNull_NoError() { + mProvider.setUpSearchMenu(null,null); + } + + @Test + public void testSetUpMenu_HasItemAdded() { + mProvider.setUpSearchMenu(menu, mActivity); + + verify(menu).add(anyInt(),anyInt(), anyInt(), anyString()); + } +} diff --git a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java new file mode 100644 index 00000000000..c2ec49c5e8f --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2016 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.graphics.drawable.Drawable; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.search2.IntentPayload; +import com.android.settings.search2.ResultPayload; +import com.android.settings.search2.SearchResult; +import com.android.settings.search2.SearchResult.Builder; +import com.android.settings.R; + +import java.util.ArrayList; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SearchResultBuilderTest { + + private Builder mBuilder; + private String mTitle; + private String mSummary; + private ArrayList mBreadcrumbs; + private int mRank; + private ResultPayload mResultPayload; + private Drawable mIcon; + + @Before + public void setUp() { + mBuilder = new Builder(); + mTitle = "title"; + mSummary = "summary"; + mBreadcrumbs = new ArrayList<>(); + mRank = 3; + mResultPayload = new IntentPayload(null); + + final Context context = ShadowApplication.getInstance().getApplicationContext(); + mIcon = context.getDrawable(R.drawable.ic_search_history); + } + + @Test + public void testAllInfo_BuildSearchResult() { + mBuilder.addTitle(mTitle) + .addSummary(mSummary) + .addRank(mRank) + .addBreadcrumbs(mBreadcrumbs) + .addIcon(mIcon) + .addPayload(mResultPayload); + SearchResult result = mBuilder.build(); + + assertThat(result).isNotNull(); + assertThat(result.title).isEqualTo(mTitle); + assertThat(result.summary).isEqualTo(mSummary); + assertThat(result.rank).isEqualTo(mRank); + assertThat(result.breadcrumbs).isEqualTo(mBreadcrumbs); + assertThat(result.icon).isEqualTo(mIcon); + assertThat(result.payload).isEqualTo(mResultPayload); + } + + @Test + public void testNoTitle_BuildSearchResultException() { + mBuilder.addSummary(mSummary) + .addRank(mRank) + .addBreadcrumbs(mBreadcrumbs) + .addIcon(mIcon) + .addPayload(mResultPayload); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } + + @Test + public void testNoSummary_BuildSearchResultException() { + mBuilder.addTitle(mTitle) + .addRank(mRank) + .addBreadcrumbs(mBreadcrumbs) + .addIcon(mIcon) + .addPayload(mResultPayload); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } + + @Test + public void testNoRank_BuildSearchResultException() { + mBuilder.addTitle(mTitle) + .addSummary(mSummary) + .addBreadcrumbs(mBreadcrumbs) + .addIcon(mIcon) + .addPayload(mResultPayload); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } + + @Test + public void testNoBreadcrumbs_BuildSearchResultException() { + mBuilder.addTitle(mTitle) + .addSummary(mSummary) + .addRank(mRank) + .addIcon(mIcon) + .addPayload(mResultPayload); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } + + @Test + public void testNoIcon_BuildSearchResultException() { + mBuilder.addTitle(mTitle) + .addSummary(mSummary) + .addRank(mRank) + .addBreadcrumbs(mBreadcrumbs) + .addPayload(mResultPayload); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } + + @Test + public void testNoPayload_BuildSearchResultException() { + mBuilder.addTitle(mTitle) + .addSummary(mSummary) + .addRank(mRank) + .addBreadcrumbs(mBreadcrumbs) + .addIcon(mIcon); + + SearchResult result = null; + try { + result = mBuilder.build(); + } catch (IllegalArgumentException e) { + // passes. + } + assertThat(result).isNull(); + } +} + + diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java index 35288630ac4..f702cdce11b 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -25,6 +25,7 @@ import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.SupportFeatureProvider; +import com.android.settings.search2.SearchFeatureProvider; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; @@ -43,6 +44,7 @@ public class FakeFeatureFactory extends FeatureFactory { public final LocaleFeatureProvider localeFeatureProvider; public final ApplicationFeatureProvider applicationFeatureProvider; public final EnterprisePrivacyFeatureProvider enterprisePrivacyFeatureProvider; + public final SearchFeatureProvider searchFeatureProvider; /** * Call this in {@code @Before} method of the test class to use fake factory. @@ -72,6 +74,7 @@ public class FakeFeatureFactory extends FeatureFactory { localeFeatureProvider = mock(LocaleFeatureProvider.class); applicationFeatureProvider = mock(ApplicationFeatureProvider.class); enterprisePrivacyFeatureProvider = mock(EnterprisePrivacyFeatureProvider.class); + searchFeatureProvider = mock(SearchFeatureProvider.class); } @Override @@ -108,4 +111,9 @@ public class FakeFeatureFactory extends FeatureFactory { public EnterprisePrivacyFeatureProvider getEnterprisePrivacyFeatureProvider(Context context) { return enterprisePrivacyFeatureProvider; } + + @Override + public SearchFeatureProvider getSearchFeatureProvider(Context context) { + return searchFeatureProvider; + } }