Fork Search code to independantly develop and test search.

This is the start of the new search in Settings. It is a nearly complete
replacement of the old search code in a more modular and flexible
architecture. It is expanding the datasources that it queries, including
the same Settings database, which will now include more first party apps
and be extended to support inline results where the user can change
settings directly from the search view. Search will also fan out to
query new sources (local or remote), and is built in a way
such that adding additional sources is roughly the same amount of work
had they been added in the initial writing of this code.

Query interpretation will now be source-dependant, allowing for future
upgrades to fuzzy search where it is applicable.

Change-Id: Ib0bac1fe92bf8a662d33abf9a99bb6ee2090ec8f
Fixes: 32115225, 32378927
Test: make RunSettingsRoboTests
This commit is contained in:
Matthew Fritze
2016-10-24 10:12:49 -07:00
parent c26f6d3ab7
commit 0ed37c3513
29 changed files with 1861 additions and 8 deletions

View File

@@ -216,6 +216,11 @@
</intent-filter>
</activity>
<activity android:name=".search2.SearchActivity"
android:label="@string/search_settings"
android:icon="@drawable/ic_search_history">
</activity>
<!-- Top-level settings -->
<activity android:name="Settings$WifiSettingsActivity"

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingEnd="?android:attr/scrollbarSize"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/search_result_item_image_size"
android:layout_height="@dimen/search_result_item_image_size"
android:layout_marginStart="@dimen/search_result_item_image_margin_start"
android:layout_marginEnd="@dimen/search_result_item_image_margin_end"
android:scaleType="centerInside"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:orientation="vertical">
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright 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.
*/
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_content"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@color/material_grey_300"/>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/search_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical">
<LinearLayout android:id="@+id/layout_recent_searches"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<!-- Padding is included in the background -->
<android.support.v7.widget.RecyclerView android:id="@+id/list_recent_searches"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/dashboard_padding_start"
android:paddingEnd="@dimen/dashboard_padding_end"
android:paddingTop="@dimen/dashboard_padding_top"
android:paddingBottom="@dimen/dashboard_padding_bottom"
android:scrollbarStyle="outsideOverlay"
android:headerDividersEnabled="false"
android:background="@drawable/search_panel_list_background"
android:elevation="@dimen/search_panel_elevation"/>
</LinearLayout>
<LinearLayout android:id="@+id/layout_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical">
<!-- Padding is included in the background -->
<android.support.v7.widget.RecyclerView android:id="@+id/list_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/dashboard_padding_start"
android:paddingEnd="@dimen/dashboard_padding_end"
android:paddingTop="@dimen/dashboard_padding_top"
android:paddingBottom="@dimen/dashboard_padding_bottom"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
android:background="@drawable/search_panel_list_background"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/search"
android:title="@string/search_menu"
android:icon="@*android:drawable/ic_search_api_material"
android:showAsAction="collapseActionView|ifRoom"
android:actionViewClass="android.widget.SearchView"/>
</menu>

View File

@@ -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<DashboardCategory> mCategories = new ArrayList<DashboardCategory>();
@@ -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;

View File

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

View File

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

View File

@@ -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<List<SearchResult>> {
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<SearchResult> result) {
// TODO Search
}
@Override
public List<SearchResult> 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<SearchResult> parseCursorForSearch(Cursor cursorResults) {
if (cursorResults == null) {
return null;
}
final ArrayList<SearchResult> 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<String> 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;
}
}

View File

@@ -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<InlineSliderPayload> CREATOR = new Creator<InlineSliderPayload>() {
@Override
public InlineSliderPayload createFromParcel(Parcel in) {
return new InlineSliderPayload(in);
}
@Override
public InlineSliderPayload[] newArray(int size) {
return new InlineSliderPayload[size];
}
};
}

View File

@@ -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<IntentPayload> CREATOR = new Creator<IntentPayload>() {
@Override
public IntentPayload createFromParcel(Parcel in) {
return new IntentPayload(in);
}
@Override
public IntentPayload[] newArray(int size) {
return new IntentPayload[size];
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<List<SearchResult>> {
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<List<SearchResult>> 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<List<SearchResult>> loader, List<SearchResult> data) {
if (data == null) {
return;
}
mSearchAdapter.mergeResults(data, loader.getClass().getName());
}
@Override
public void onLoaderReset(Loader<List<SearchResult>> loader) { }
@Override
public int getMetricsCategory() {
return MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS;
}
private void clearLoaders() {
if (mSearchLoader != null) {
mSearchLoader.cancelLoad();
mSearchLoader = null;
}
}
}

View File

@@ -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<SearchResult> {
@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<String> 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<String> 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<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

@@ -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<SearchViewHolder> {
private ArrayList<SearchResult> mSearchResults;
private HashMap<String, List<SearchResult>> mResultsMap;
public SearchResultsAdapter() {
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 = mergeMappedResults();
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());
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<SearchResult> getSearchResults() {
return mSearchResults;
}
}

View File

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

View File

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

View File

@@ -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<SearchResult> results = mLoader.parseCursorForSearch(null);
assertThat(results).isNull();
}
@Test
public void testParseCursor_NotNull() {
List<SearchResult> results = mLoader.parseCursorForSearch(getDummyCursor());
assertThat(results).isNotNull();
}
@Test
public void testParseCursor_MatchesRank() {
List<SearchResult> results = mLoader.parseCursorForSearch(getDummyCursor());
for (int i = 0; i < EXAMPLES; i++) {
assertThat(results.get(i).rank).isEqualTo(i);
}
}
@Test
public void testParseCursor_MatchesTitle() {
List<SearchResult> 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<SearchResult> results = mLoader.parseCursorForSearch(getDummyCursor());
for (int i = 0; i < EXAMPLES; i++) {
assertThat(results.get(i).summary).isEqualTo(SUMMARY);
}
}
@Test
public void testParseCursor_MatchesIcon() {
List<SearchResult> 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<SearchResult> 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<SearchResult> 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<String> 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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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<SearchResult> getIntentSampleResults() {
ArrayList<SearchResult> sampleResults = new ArrayList<>();
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();
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<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).isEmpty();
}
@Test
public void testSingleSourceMerge_ExactCopyReturned() {
ArrayList<SearchResult> intentResults = getIntentSampleResults();
mAdapter.mergeResults(intentResults, mLoaderClassName);
ArrayList<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).containsAllIn(intentResults);
}
@Test
public void testDuplicateSourceMerge_ExactCopyReturned() {
ArrayList<SearchResult> intentResults = getIntentSampleResults();
mAdapter.mergeResults(intentResults, mLoaderClassName);
mAdapter.mergeResults(intentResults, mLoaderClassName);
ArrayList<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).containsAllIn(intentResults);
}
}

View File

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

View File

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

View File

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