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; + } }