diff --git a/src/com/android/settings/applications/PackageManagerWrapper.java b/src/com/android/settings/applications/PackageManagerWrapper.java index d399115b054..6c783d80736 100644 --- a/src/com/android/settings/applications/PackageManagerWrapper.java +++ b/src/com/android/settings/applications/PackageManagerWrapper.java @@ -18,6 +18,7 @@ package com.android.settings.applications; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import java.util.List; @@ -29,24 +30,30 @@ import java.util.List; * the API version supported by Robolectric. */ public interface PackageManagerWrapper { + + /** + * Returns the real {@code PackageManager} object. + */ + PackageManager getPackageManager(); + /** * Calls {@code PackageManager.getInstalledApplicationsAsUser()}. * - * @see android.content.pm.PackageManager.PackageManager#getInstalledApplicationsAsUser + * @see android.content.pm.PackageManager#getInstalledApplicationsAsUser */ List getInstalledApplicationsAsUser(int flags, int userId); /** * Calls {@code PackageManager.hasSystemFeature()}. * - * @see android.content.pm.PackageManager.PackageManager#hasSystemFeature + * @see android.content.pm.PackageManager#hasSystemFeature */ boolean hasSystemFeature(String name); /** * Calls {@code PackageManager.queryIntentActivitiesAsUser()}. * - * @see android.content.pm.PackageManager.PackageManager#queryIntentActivitiesAsUser + * @see android.content.pm.PackageManager#queryIntentActivitiesAsUser */ List queryIntentActivitiesAsUser(Intent intent, int flags, int userId); } diff --git a/src/com/android/settings/applications/PackageManagerWrapperImpl.java b/src/com/android/settings/applications/PackageManagerWrapperImpl.java index 89668698110..db1d30a7d60 100644 --- a/src/com/android/settings/applications/PackageManagerWrapperImpl.java +++ b/src/com/android/settings/applications/PackageManagerWrapperImpl.java @@ -24,12 +24,18 @@ import android.content.pm.ResolveInfo; import java.util.List; public class PackageManagerWrapperImpl implements PackageManagerWrapper { + private final PackageManager mPm; public PackageManagerWrapperImpl(PackageManager pm) { mPm = pm; } + @Override + public PackageManager getPackageManager() { + return mPm; + } + @Override public List getInstalledApplicationsAsUser(int flags, int userId) { return mPm.getInstalledApplicationsAsUser(flags, userId); diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java index aca94b1a471..a4e614f3de2 100644 --- a/src/com/android/settings/search2/DatabaseResultLoader.java +++ b/src/com/android/settings/search2/DatabaseResultLoader.java @@ -23,10 +23,11 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; import android.support.annotation.VisibleForTesting; + +import com.android.settings.R; 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; @@ -107,7 +108,6 @@ public class DatabaseResultLoader extends AsyncLoader> { icon = mContext.getDrawable(R.drawable.ic_search_history); } - SearchResult.Builder builder = new SearchResult.Builder(); builder.addTitle(title) .addSummary(summaryOn) diff --git a/src/com/android/settings/search2/InstalledAppResultLoader.java b/src/com/android/settings/search2/InstalledAppResultLoader.java new file mode 100644 index 00000000000..449e52c80f8 --- /dev/null +++ b/src/com/android/settings/search2/InstalledAppResultLoader.java @@ -0,0 +1,139 @@ +/* + * 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.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.net.Uri; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.settings.applications.PackageManagerWrapper; +import com.android.settings.utils.AsyncLoader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Search loader for installed apps. + */ +public class InstalledAppResultLoader extends AsyncLoader> { + + private static final int NAME_NO_MATCH = -1; + private static final int NAME_EXACT_MATCH = 0; + + private final String mQuery; + private final UserManager mUserManager; + private final PackageManagerWrapper mPackageManager; + + public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper, + String query) { + super(context); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mPackageManager = pmWrapper; + mQuery = query; + } + + @Override + public List loadInBackground() { + final List results = new ArrayList<>(); + final PackageManager pm = mPackageManager.getPackageManager(); + + for (UserInfo user : getUsersToCount()) { + final List apps = + mPackageManager.getInstalledApplicationsAsUser( + PackageManager.MATCH_DISABLED_COMPONENTS + | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + | (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0), + user.id); + for (ApplicationInfo info : apps) { + if (info.isSystemApp()) { + continue; + } + final CharSequence label = info.loadLabel(pm); + final int wordDiff = getWordDifference(label.toString(), mQuery); + if (wordDiff == NAME_NO_MATCH) { + continue; + } + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", info.packageName, null)); + + final SearchResult.Builder builder = new SearchResult.Builder(); + builder.addIcon(info.loadIcon(pm)) + .addTitle(info.loadLabel(pm)) + .addRank(wordDiff) + .addPayload(new IntentPayload(intent)); + results.add(builder.build()); + } + } + Collections.sort(results); + return results; + } + + @Override + protected void onDiscardResult(List result) { + + } + + private List getUsersToCount() { + return mUserManager.getProfiles(UserHandle.myUserId()); + } + + /** + * Returns "difference" between appName and query string. appName must contain all + * characters from query, in the same order. If not, returns NAME_NO_MATCH. If they do match, + * returns an int value representing how different they are, NAME_EXACT_MATCH means they match + * perfectly, and larger values means they are less similar. + *

+ * Example: + * appName: Abcde, query: Abcde, Returns NAME_EXACT_MATCH + * appName: Abcde, query: ade, Returns 2 + * appName: Abcde, query: ae, Returns 3 + * appName: Abcde, query: ea, Returns NAME_NO_MATCH + * appName: Abcde, query: xyz, Returns NAME_NO_MATCH + */ + private int getWordDifference(String appName, String query) { + if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(query)) { + return NAME_NO_MATCH; + } + final char[] queryTokens = query.toString().toLowerCase().toCharArray(); + final char[] valueText = appName.toLowerCase().toCharArray(); + if (queryTokens.length > valueText.length) { + return NAME_NO_MATCH; + } + int i = 0; + int j = 0; + while (i < valueText.length && j < queryTokens.length) { + if (valueText[i++] == queryTokens[j]) { + j++; + } + } + if (j != queryTokens.length) { + return NAME_NO_MATCH; + } + // Use the diff in length as a proxy of how close the 2 words match. Value range from 0 + // to infinity. + return valueText.length - queryTokens.length; + } +} diff --git a/src/com/android/settings/search2/IntentSearchViewHolder.java b/src/com/android/settings/search2/IntentSearchViewHolder.java index 0b99d6e58d5..0187c1c1dea 100644 --- a/src/com/android/settings/search2/IntentSearchViewHolder.java +++ b/src/com/android/settings/search2/IntentSearchViewHolder.java @@ -15,9 +15,11 @@ */ package com.android.settings.search2; +import android.app.Fragment; import android.view.View; import android.widget.ImageView; import android.widget.TextView; + import com.android.settings.R; /** @@ -25,6 +27,7 @@ import com.android.settings.R; * 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; @@ -33,12 +36,19 @@ public class IntentSearchViewHolder extends SearchViewHolder { super(view); titleView = (TextView) view.findViewById(R.id.title); summaryView = (TextView) view.findViewById(R.id.summary); - iconView= (ImageView) view.findViewById(R.id.icon); + iconView = (ImageView) view.findViewById(R.id.icon); } - public void onBind(SearchResult result) { + @Override + public void onBind(Fragment fragment, SearchResult result) { titleView.setText(result.title); summaryView.setText(result.summary); iconView.setImageDrawable(result.icon); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + fragment.startActivity(((IntentPayload) result.payload).intent); + } + }); } } diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java index 569a627c338..da29c85da4e 100644 --- a/src/com/android/settings/search2/SearchFeatureProvider.java +++ b/src/com/android/settings/search2/SearchFeatureProvider.java @@ -41,4 +41,9 @@ public interface SearchFeatureProvider { * Returns a new loader to search in index database. */ DatabaseResultLoader getDatabaseSearchLoader(Context context, String query); + + /** + * Returns a new loader to search installed apps. + */ + InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query); } diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java index 81a41dccd5b..72030493375 100644 --- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java @@ -19,14 +19,11 @@ 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; -import com.android.settings.utils.AsyncLoader; -import java.util.List; +import com.android.settings.R; +import com.android.settings.applications.PackageManagerWrapperImpl; /** * FeatureProvider for the refactored search code. @@ -51,15 +48,15 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { } 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; - } - }); + .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); } @@ -68,4 +65,10 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) { return new DatabaseResultLoader(context, query); } + + @Override + public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) { + return new InstalledAppResultLoader( + context, new PackageManagerWrapperImpl(context.getPackageManager()), query); + } } diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java index 1fb123cb2c9..fca52e9cb15 100644 --- a/src/com/android/settings/search2/SearchFragment.java +++ b/src/com/android/settings/search2/SearchFragment.java @@ -46,13 +46,13 @@ public class SearchFragment extends InstrumentedFragment implements static final String STATE_QUERY = "query"; // Loader IDs - private static final int DATABASE_LOADER_ID = 0; + private static final int LOADER_ID_DATABASE = 0; + private static final int LOADER_ID_INSTALLED_APPS = 1; @VisibleForTesting String mQuery; private SearchFeatureProvider mSearchFeatureProvider; - private DatabaseResultLoader mSearchLoader; private SearchResultsAdapter mSearchAdapter; private RecyclerView mResultsRecyclerView; @@ -73,10 +73,12 @@ public class SearchFragment extends InstrumentedFragment implements public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); - mSearchAdapter = new SearchResultsAdapter(); + mSearchAdapter = new SearchResultsAdapter(this); if (savedInstanceState != null) { mQuery = savedInstanceState.getString(STATE_QUERY); - getLoaderManager().initLoader(DATABASE_LOADER_ID, null, this); + final LoaderManager loaderManager = getLoaderManager(); + loaderManager.initLoader(LOADER_ID_DATABASE, null, this); + loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this); } final ActionBar actionBar = getActivity().getActionBar(); actionBar.setCustomView(makeSearchView(actionBar, mQuery)); @@ -109,7 +111,7 @@ public class SearchFragment extends InstrumentedFragment implements mSearchAdapter.clearResults(); if (TextUtils.isEmpty(mQuery)) { - getLoaderManager().destroyLoader(DATABASE_LOADER_ID); + getLoaderManager().destroyLoader(LOADER_ID_DATABASE); } else { restartLoaders(); } @@ -127,9 +129,10 @@ public class SearchFragment extends InstrumentedFragment implements final Activity activity = getActivity(); switch (id) { - case DATABASE_LOADER_ID: - mSearchLoader = mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery); - return mSearchLoader; + case LOADER_ID_DATABASE: + return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery); + case LOADER_ID_INSTALLED_APPS: + return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery); default: return null; } @@ -137,10 +140,6 @@ public class SearchFragment extends InstrumentedFragment implements @Override public void onLoadFinished(Loader> loader, List data) { - if (data == null) { - return; - } - mSearchAdapter.mergeResults(data, loader.getClass().getName()); } @@ -150,7 +149,8 @@ public class SearchFragment extends InstrumentedFragment implements private void restartLoaders() { final LoaderManager loaderManager = getLoaderManager(); - loaderManager.restartLoader(DATABASE_LOADER_ID, null /* args */, this /* callback */); + loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */); + loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */); } private SearchView makeSearchView(ActionBar actionBar, String query) { diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java index e483df3021a..9fb250f5d48 100644 --- a/src/com/android/settings/search2/SearchResult.java +++ b/src/com/android/settings/search2/SearchResult.java @@ -19,11 +19,73 @@ package com.android.settings.search2; import android.graphics.drawable.Drawable; import java.util.ArrayList; +import java.util.Objects; /** - * Dataclass as an interface for all Search Results. + * Data class as an interface for all Search Results. */ public class SearchResult implements Comparable { + + /** + * The title of the result and main text displayed. + * Intent Results: Displays as the primary + */ + public final CharSequence title; + + /** + * Summary / subtitle text + * Intent Results: Displays the text underneath the title + */ + final public CharSequence 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; + + /** + * Stable id for this object. + */ + public final long stableId; + + 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(); + stableId = Objects.hash(title, summary, breadcrumbs, rank, icon, payload, viewType); + + } + @Override public int compareTo(SearchResult searchResult) { if (searchResult == null) { @@ -33,19 +95,19 @@ public class SearchResult implements Comparable { } public static class Builder { - protected String mTitle; - protected String mSummary; + protected CharSequence mTitle; + protected CharSequence mSummary; protected ArrayList mBreadcrumbs; protected int mRank = -1; protected ResultPayload mResultPayload; protected Drawable mIcon; - public Builder addTitle(String title) { + public Builder addTitle(CharSequence title) { mTitle = title; return this; } - public Builder addSummary(String summary) { + public Builder addSummary(CharSequence summary) { mSummary = summary; return this; } @@ -77,10 +139,6 @@ public class SearchResult implements Comparable { // Check that all of the mandatory fields are set. if (mTitle == null) { throw new IllegalArgumentException("SearchResult missing title argument"); - } else if (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) { @@ -91,56 +149,4 @@ public class SearchResult implements Comparable { 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 index 62b79b32d78..b5884968747 100644 --- a/src/com/android/settings/search2/SearchResultsAdapter.java +++ b/src/com/android/settings/search2/SearchResultsAdapter.java @@ -33,40 +33,19 @@ import java.util.Map; public class SearchResultsAdapter extends Adapter { private final List mSearchResults; private final Map> mResultsMap; + private final SearchFragment mFragment; - public SearchResultsAdapter() { + public SearchResultsAdapter(SearchFragment fragment) { + mFragment = fragment; mSearchResults = new ArrayList<>(); mResultsMap = new HashMap<>(); setHasStableIds(true); } - public void mergeResults(List freshResults, String loaderClassName) { - if (freshResults == null) { - return; - } - mResultsMap.put(loaderClassName, freshResults); - mSearchResults.addAll(mergeMappedResults()); - notifyDataSetChanged(); - } - - public void clearResults() { - mSearchResults.clear(); - mResultsMap.clear(); - 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()); + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); switch (viewType) { case PayloadType.INTENT: View view = inflater.inflate(R.layout.search_intent_item, parent, false); @@ -82,13 +61,12 @@ public class SearchResultsAdapter extends Adapter { @Override public void onBindViewHolder(SearchViewHolder holder, int position) { - SearchResult result = mSearchResults.get(position); - holder.onBind(result); + holder.onBind(mFragment, mSearchResults.get(position)); } @Override public long getItemId(int position) { - return super.getItemId(position); + return mSearchResults.get(position).stableId; } @Override @@ -101,6 +79,23 @@ public class SearchResultsAdapter extends Adapter { return mSearchResults.size(); } + public void mergeResults(List freshResults, String loaderClassName) { + if (freshResults == null) { + return; + } + mResultsMap.put(loaderClassName, freshResults); + final int oldSize = mSearchResults.size(); + mSearchResults.addAll(freshResults); + final int newSize = mSearchResults.size(); + notifyItemRangeInserted(oldSize, newSize - oldSize); + } + + public void clearResults() { + mSearchResults.clear(); + mResultsMap.clear(); + notifyDataSetChanged(); + } + @VisibleForTesting public List getSearchResults() { return mSearchResults; diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java index 2f500fb7ede..45ceb3884a5 100644 --- a/src/com/android/settings/search2/SearchViewHolder.java +++ b/src/com/android/settings/search2/SearchViewHolder.java @@ -15,6 +15,7 @@ */ package com.android.settings.search2; +import android.app.Fragment; import android.support.v7.widget.RecyclerView; import android.view.View; @@ -29,5 +30,5 @@ public abstract class SearchViewHolder extends RecyclerView.ViewHolder { super(view); } - public abstract void onBind(SearchResult result); + public abstract void onBind(Fragment fragment, SearchResult result); } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java index 2534c0b2b9f..805c7cb1c15 100644 --- a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java +++ b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java @@ -17,40 +17,50 @@ package com.android.settings.search; +import android.app.Fragment; import android.content.Context; +import android.content.Intent; 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 com.android.settings.search2.SearchResult.Builder; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; import java.util.ArrayList; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; @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"; + @Mock + private Fragment mFragment; + private IntentSearchViewHolder mHolder; + private Drawable mIcon; @Before public void setUp() { + MockitoAnnotations.initMocks(this); final Context context = ShadowApplication.getInstance().getApplicationContext(); View view = LayoutInflater.from(context).inflate(R.layout.search_intent_item, null); mHolder = new IntentSearchViewHolder(view); @@ -68,11 +78,13 @@ public class IntentSearchViewHolderTest { @Test public void testBindViewElements_AllUpdated() { SearchResult result = getSearchResult(); - mHolder.onBind(result); + mHolder.onBind(mFragment, result); + mHolder.itemView.performClick(); assertThat(mHolder.titleView.getText()).isEqualTo(TITLE); assertThat(mHolder.summaryView.getText()).isEqualTo(SUMMARY); assertThat(mHolder.iconView.getDrawable()).isEqualTo(mIcon); + verify(mFragment).startActivity(any(Intent.class)); } private SearchResult getSearchResult() { @@ -81,7 +93,7 @@ public class IntentSearchViewHolderTest { .addSummary(SUMMARY) .addRank(1) .addPayload(new IntentPayload(null)) - .addBreadcrumbs(new ArrayList()) + .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 index 81e918054ea..0756d3f218a 100644 --- a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java @@ -27,6 +27,7 @@ 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.SearchFragment; import com.android.settings.search2.SearchResult; import com.android.settings.search2.SearchResult.Builder; import com.android.settings.search2.SearchResultsAdapter; @@ -34,6 +35,8 @@ import com.android.settings.search2.SearchResultsAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; @@ -46,14 +49,17 @@ import static com.google.common.truth.Truth.assertThat; @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class SearchAdapterTest { + @Mock + private SearchFragment mFragment; private SearchResultsAdapter mAdapter; private Context mContext; private String mLoaderClassName; @Before public void setUp() { + MockitoAnnotations.initMocks(this); mContext = Robolectric.buildActivity(Activity.class).get(); - mAdapter = new SearchResultsAdapter(); + mAdapter = new SearchResultsAdapter(mFragment); mLoaderClassName = DatabaseResultLoader.class.getName(); } @@ -62,8 +68,7 @@ public class SearchAdapterTest { 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(); + final SearchResult.Builder builder = new Builder(); builder.addTitle("title") .addSummary("summary") .addRank(1) diff --git a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java index c2ec49c5e8f..a0f4cc52c48 100644 --- a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java @@ -19,23 +19,23 @@ package com.android.settings.search; import android.content.Context; import android.graphics.drawable.Drawable; + +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.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 java.util.ArrayList; + import static com.google.common.truth.Truth.assertThat; @RunWith(SettingsRobolectricTestRunner.class) @@ -99,23 +99,6 @@ public class SearchResultBuilderTest { 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) @@ -133,23 +116,6 @@ public class SearchResultBuilderTest { 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) diff --git a/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java new file mode 100644 index 00000000000..e3c2180c1b5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java @@ -0,0 +1,107 @@ +/* + * 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.pm.UserInfo; +import android.os.UserManager; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.applications.PackageManagerWrapper; +import com.android.settings.testutils.ApplicationTestUtils; + +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.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class InstalledAppResultLoaderTest { + + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PackageManagerWrapper mPackageManagerWrapper; + @Mock + private UserManager mUserManager; + + private InstalledAppResultLoader mLoader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + final List infos = new ArrayList<>(); + infos.add(new UserInfo(1, "user 1", 0)); + when(mUserManager.getProfiles(anyInt())).thenReturn(infos); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) + .thenReturn(Arrays.asList( + ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM), + ApplicationTestUtils.buildInfo(0 /* uid */, "app2", FLAG_SYSTEM), + ApplicationTestUtils.buildInfo(0 /* uid */, "app3", FLAG_SYSTEM), + ApplicationTestUtils.buildInfo(0 /* uid */, "app4", 0 /* flags */), + ApplicationTestUtils.buildInfo(0 /* uid */, "app", 0 /* flags */))); + } + + @Test + public void query_noMatchingQuery_shouldReturnEmptyResult() { + final String query = "abc"; + + mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query); + + assertThat(mLoader.loadInBackground()).isEmpty(); + } + + @Test + public void query_matchingQuery_shouldReturnNonSystemApps() { + final String query = "app"; + + mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query); + + assertThat(mLoader.loadInBackground().size()).isEqualTo(2); + } + + @Test + public void query_matchingQuery_shouldRankBasedOnSimilarity() { + final String query = "app"; + + mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query); + final List results = mLoader.loadInBackground(); + + // List is sorted by rank + assertThat(results.get(0).rank).isLessThan(results.get(1).rank); + // perfect match first + assertThat(results.get(0).title).isEqualTo(query); + // Then partial match + assertThat(results.get(1).title).isNotEqualTo(query); + } +} diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java index 979b7e5dde6..40d1ae59a11 100644 --- a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java +++ b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java @@ -47,6 +47,8 @@ public class SearchFragmentTest { private Context mContext; @Mock private DatabaseResultLoader mDatabaseResultLoader; + @Mock + private InstalledAppResultLoader mInstalledAppResultLoader; private FakeFeatureFactory mFeatureFactory; @Before @@ -54,14 +56,16 @@ public class SearchFragmentTest { MockitoAnnotations.initMocks(this); FakeFeatureFactory.setupForTest(mContext); mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext); + when(mFeatureFactory.searchFeatureProvider + .getDatabaseSearchLoader(any(Context.class), anyString())) + .thenReturn(mDatabaseResultLoader); + when(mFeatureFactory.searchFeatureProvider + .getInstalledAppSearchLoader(any(Context.class), anyString())) + .thenReturn(mInstalledAppResultLoader); } @Test public void screenRotate_shouldPersistQuery() { - when(mFeatureFactory.searchFeatureProvider - .getDatabaseSearchLoader(any(Context.class), anyString())) - .thenReturn(mDatabaseResultLoader); - final Bundle bundle = new Bundle(); final String testQuery = "test"; ActivityController activityController = @@ -79,14 +83,12 @@ public class SearchFragmentTest { verify(mFeatureFactory.searchFeatureProvider) .getDatabaseSearchLoader(any(Context.class), anyString()); + verify(mFeatureFactory.searchFeatureProvider) + .getInstalledAppSearchLoader(any(Context.class), anyString()); } @Test public void queryTextChange_shouldTriggerLoader() { - when(mFeatureFactory.searchFeatureProvider - .getDatabaseSearchLoader(any(Context.class), anyString())) - .thenReturn(mDatabaseResultLoader); - final String testQuery = "test"; ActivityController activityController = Robolectric.buildActivity(SearchActivity.class); @@ -98,5 +100,7 @@ public class SearchFragmentTest { verify(mFeatureFactory.searchFeatureProvider) .getDatabaseSearchLoader(any(Context.class), anyString()); + verify(mFeatureFactory.searchFeatureProvider) + .getInstalledAppSearchLoader(any(Context.class), anyString()); } }