Add search loader for installed apps.

- The loader filters out system apps.
- Loader performs case-insensitive match with app names.
- SearchResultAdapter combines results from multiple loaders into a
  single list.

Fixes: 33347966
Test: make RunSettingsRoboTests
Change-Id: I228ca6fb82f0ac5151b2346c079c2de41104a4df
This commit is contained in:
Fan Zhang
2016-12-06 16:22:16 -08:00
parent 413eaa4070
commit 36d0d143be
16 changed files with 445 additions and 179 deletions

View File

@@ -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<ApplicationInfo> 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<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId);
}

View File

@@ -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<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) {
return mPm.getInstalledApplicationsAsUser(flags, userId);

View File

@@ -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<List<SearchResult>> {
icon = mContext.getDrawable(R.drawable.ic_search_history);
}
SearchResult.Builder builder = new SearchResult.Builder();
builder.addTitle(title)
.addSummary(summaryOn)

View File

@@ -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<List<SearchResult>> {
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<SearchResult> loadInBackground() {
final List<SearchResult> results = new ArrayList<>();
final PackageManager pm = mPackageManager.getPackageManager();
for (UserInfo user : getUsersToCount()) {
final List<ApplicationInfo> 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<SearchResult> result) {
}
private List<UserInfo> 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.
* <p/>
* 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;
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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);
}

View File

@@ -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.
@@ -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);
}
}

View File

@@ -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<List<SearchResult>> loader, List<SearchResult> 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) {

View File

@@ -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<SearchResult> {
/**
* 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<String> 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<SearchResult> {
}
public static class Builder {
protected String mTitle;
protected String mSummary;
protected CharSequence mTitle;
protected CharSequence mSummary;
protected ArrayList<String> 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<SearchResult> {
// 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<SearchResult> {
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<String> 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();
}
}

View File

@@ -33,40 +33,19 @@ import java.util.Map;
public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
private final List<SearchResult> mSearchResults;
private final Map<String, List<SearchResult>> 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<SearchResult> 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<SearchResult> mergeMappedResults() {
ArrayList<SearchResult> 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<SearchViewHolder> {
@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<SearchViewHolder> {
return mSearchResults.size();
}
public void mergeResults(List<SearchResult> 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<SearchResult> getSearchResults() {
return mSearchResults;

View File

@@ -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);
}

View File

@@ -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<String>())
.addBreadcrumbs(new ArrayList<>())
.addIcon(mIcon);
return builder.build();

View File

@@ -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<String> 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)

View File

@@ -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)

View File

@@ -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<UserInfo> 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<SearchResult> 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);
}
}

View File

@@ -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<SearchActivity> 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<SearchActivity> 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());
}
}