OSDN Git Service

Fork Search code to independantly develop and test search.
authorMatthew Fritze <mfritze@google.com>
Mon, 24 Oct 2016 17:12:49 +0000 (10:12 -0700)
committerMatthew Fritze <mfritze@google.com>
Thu, 17 Nov 2016 22:34:29 +0000 (14:34 -0800)
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: 3211522532378927
Test: make RunSettingsRoboTests

29 files changed:
AndroidManifest.xml
res/layout/search_intent_item.xml [new file with mode: 0644]
res/layout/search_main.xml [new file with mode: 0644]
res/layout/search_panel_2.xml [new file with mode: 0644]
res/menu/search_options_menu.xml [new file with mode: 0644]
src/com/android/settings/SettingsActivity.java
src/com/android/settings/overlay/FeatureFactory.java
src/com/android/settings/overlay/FeatureFactoryImpl.java
src/com/android/settings/search2/DatabaseResultLoader.java [new file with mode: 0644]
src/com/android/settings/search2/InlineSliderPayload.java [new file with mode: 0644]
src/com/android/settings/search2/IntentPayload.java [new file with mode: 0644]
src/com/android/settings/search2/IntentSearchViewHolder.java [new file with mode: 0644]
src/com/android/settings/search2/ResultPayload.java [new file with mode: 0644]
src/com/android/settings/search2/SearchActivity.java [new file with mode: 0644]
src/com/android/settings/search2/SearchFeatureProvider.java [new file with mode: 0644]
src/com/android/settings/search2/SearchFeatureProviderImpl.java [new file with mode: 0644]
src/com/android/settings/search2/SearchFragment.java [new file with mode: 0644]
src/com/android/settings/search2/SearchResult.java [new file with mode: 0644]
src/com/android/settings/search2/SearchResultsAdapter.java [new file with mode: 0644]
src/com/android/settings/search2/SearchViewHolder.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java
tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/IntentPayloadTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/SearchAdapterTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java

index 4fb17f7..504f12f 100644 (file)
             </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"
diff --git a/res/layout/search_intent_item.xml b/res/layout/search_intent_item.xml
new file mode 100644 (file)
index 0000000..b68f65b
--- /dev/null
@@ -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>
diff --git a/res/layout/search_main.xml b/res/layout/search_main.xml
new file mode 100644 (file)
index 0000000..ab728cd
--- /dev/null
@@ -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"/>
diff --git a/res/layout/search_panel_2.xml b/res/layout/search_panel_2.xml
new file mode 100644 (file)
index 0000000..671c19c
--- /dev/null
@@ -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>
\ No newline at end of file
diff --git a/res/menu/search_options_menu.xml b/res/menu/search_options_menu.xml
new file mode 100644 (file)
index 0000000..25a79d4
--- /dev/null
@@ -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>
index f67f73f..1831755 100644 (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;
index 55ea4bb..75f1001 100644 (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);
index ec0ff46..4a7396e 100644 (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;
+    }
 }
diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java
new file mode 100644 (file)
index 0000000..aca94b1
--- /dev/null
@@ -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;
+    }
+
+}
diff --git a/src/com/android/settings/search2/InlineSliderPayload.java b/src/com/android/settings/search2/InlineSliderPayload.java
new file mode 100644 (file)
index 0000000..8f08d59
--- /dev/null
@@ -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];
+        }
+    };
+}
\ No newline at end of file
diff --git a/src/com/android/settings/search2/IntentPayload.java b/src/com/android/settings/search2/IntentPayload.java
new file mode 100644 (file)
index 0000000..1ef3797
--- /dev/null
@@ -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];
+        }
+    };
+
+}
\ No newline at end of file
diff --git a/src/com/android/settings/search2/IntentSearchViewHolder.java b/src/com/android/settings/search2/IntentSearchViewHolder.java
new file mode 100644 (file)
index 0000000..0b99d6e
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.search2;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.settings.R;
+
+/**
+ * ViewHolder for intent based search results.
+ * The DatabaseResultLoader is the primary use case for this ViewHolder.
+ */
+public class IntentSearchViewHolder extends SearchViewHolder {
+    public final TextView titleView;
+    public final TextView summaryView;
+    public final ImageView iconView;
+
+    public IntentSearchViewHolder(View view) {
+        super(view);
+        titleView = (TextView) view.findViewById(R.id.title);
+        summaryView = (TextView) view.findViewById(R.id.summary);
+        iconView= (ImageView) view.findViewById(R.id.icon);
+    }
+
+    public void onBind(SearchResult result) {
+        titleView.setText(result.title);
+        summaryView.setText(result.summary);
+        iconView.setImageDrawable(result.icon);
+    }
+}
diff --git a/src/com/android/settings/search2/ResultPayload.java b/src/com/android/settings/search2/ResultPayload.java
new file mode 100644 (file)
index 0000000..3a4e477
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.search2;
+
+import android.annotation.IntDef;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A interface for search results types. Examples include Inline results, third party apps
+ * or any future possibilities.
+ */
+public abstract class ResultPayload implements Parcelable {
+
+    @IntDef({PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH, PayloadType.INTENT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PayloadType {
+        int INTENT = 0;
+        int INLINE_SLIDER = 1;
+        int INLINE_SWITCH = 2;
+    }
+
+    @ResultPayload.PayloadType public abstract int getType();
+}
diff --git a/src/com/android/settings/search2/SearchActivity.java b/src/com/android/settings/search2/SearchActivity.java
new file mode 100644 (file)
index 0000000..25a54cf
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.search2;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.os.Bundle;
+
+import com.android.settings.R;
+
+public class SearchActivity extends Activity {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.search_main);
+
+        FragmentManager fragmentManager = getFragmentManager();
+        Fragment fragment = fragmentManager.findFragmentById(R.id.main_content);
+        if (fragment == null) {
+            fragmentManager.beginTransaction()
+                    .add(R.id.main_content, new SearchFragment())
+                    .commit();
+        }
+    }
+}
diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java
new file mode 100644 (file)
index 0000000..14f5d13
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.search2;
+
+import android.app.Activity;
+import android.widget.SearchView;
+import android.view.Menu;
+
+/**
+ * FeatureProvider for Settings Search
+ */
+public interface SearchFeatureProvider {
+
+    /**
+     * @return true to use the new version of search
+     */
+    boolean isEnabled();
+
+    /**
+     * Inserts the Menu items into Settings activity.
+     * @param menu Items will be inserted into this menu.
+     * @param activity The activity that precedes SearchActivity.
+     */
+    void setUpSearchMenu(Menu menu, Activity activity);
+}
diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
new file mode 100644 (file)
index 0000000..3c6dc35
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.search2;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.SearchView;
+import android.view.Menu;
+
+import android.view.MenuItem;
+import com.android.settings.R;
+
+/**
+ * FeatureProvider for the refactored search code.
+ */
+public class SearchFeatureProviderImpl implements SearchFeatureProvider {
+    protected Context mContext;
+
+
+    public SearchFeatureProviderImpl(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return false;
+    }
+
+    @Override
+    public void setUpSearchMenu(Menu menu, final Activity activity) {
+        if (menu == null || activity == null) {
+            return;
+        }
+        String menuTitle = mContext.getString(R.string.search_menu);
+        MenuItem menuItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, menuTitle)
+            .setIcon(R.drawable.abc_ic_search_api_material)
+            .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+                @Override
+                public boolean onMenuItemClick(MenuItem item) {
+                    Intent intent = new Intent(activity, SearchActivity.class);
+                    activity.startActivity(intent);
+                    return true;
+                }
+            });
+
+        menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+    }
+}
diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java
new file mode 100644 (file)
index 0000000..18f20be
--- /dev/null
@@ -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;
+        }
+    }
+}
diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java
new file mode 100644 (file)
index 0000000..e483df3
--- /dev/null
@@ -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();
+    }
+}
diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java
new file mode 100644 (file)
index 0000000..22f106b
--- /dev/null
@@ -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;
+    }
+}
diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java
new file mode 100644 (file)
index 0000000..2f500fb
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.search2;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * The ViewHolder for the Search RecyclerView.
+ * There are multiple search result types in the same Recycler view with different UI requirements.
+ * Some examples include Intent results, Inline results, and Help articles.
+ */
+public abstract class SearchViewHolder extends RecyclerView.ViewHolder {
+
+    public SearchViewHolder(View view) {
+        super(view);
+    }
+
+    public abstract void onBind(SearchResult result);
+}
\ No newline at end of file
index 1a7647d..d6d6963 100644 (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;
             }
         };
diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
new file mode 100644 (file)
index 0000000..a744bb7
--- /dev/null
@@ -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;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java b/tests/robotests/src/com/android/settings/search/InlineSliderPayloadTest.java
new file mode 100644 (file)
index 0000000..d52eb94
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.settings.search;
+
+import android.net.Uri;
+import android.os.Parcel;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.search2.InlineSliderPayload;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class InlineSliderPayloadTest {
+    private InlineSliderPayload mPayload;
+
+    @Test
+    public void testParcelOrdering_StaysValid() {
+        Uri uri = Uri.parse("http://www.TESTURI.com");
+        Parcel parcel = Parcel.obtain();
+
+        mPayload = new InlineSliderPayload(uri);
+        mPayload.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+        InlineSliderPayload newPayload = InlineSliderPayload.CREATOR.createFromParcel(parcel);
+
+        String originalUri = mPayload.uri.toString();
+        String copiedUri = newPayload.uri.toString();
+        assertThat(originalUri).isEqualTo(copiedUri);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/IntentPayloadTest.java b/tests/robotests/src/com/android/settings/search/IntentPayloadTest.java
new file mode 100644 (file)
index 0000000..6f42622
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.settings.search;
+
+import android.content.Intent;
+import android.os.Parcel;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.search2.IntentPayload;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class IntentPayloadTest {
+    private IntentPayload mPayload;
+
+    private final String EXTRA_KEY = "key";
+    private final String EXTRA_VALUE = "value";
+
+    @Test
+    public void testParcelOrdering_StaysValid() {
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_KEY, EXTRA_VALUE);
+        Parcel parcel = Parcel.obtain();
+
+        mPayload = new IntentPayload(intent);
+        mPayload.writeToParcel(parcel, 0);
+        // Reset parcel for reading
+        parcel.setDataPosition(0);
+        IntentPayload newPayload = IntentPayload.CREATOR.createFromParcel(parcel);
+
+        String originalIntentExtra = mPayload.intent.getStringExtra(EXTRA_KEY);
+        String copiedIntentExtra = newPayload.intent.getStringExtra(EXTRA_KEY);
+        assertThat(originalIntentExtra).isEqualTo(copiedIntentExtra);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java
new file mode 100644 (file)
index 0000000..2534c0b
--- /dev/null
@@ -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();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java
new file mode 100644 (file)
index 0000000..b3da4eb
--- /dev/null
@@ -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);
+    }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java
new file mode 100644 (file)
index 0000000..06f4322
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.settings.search;
+
+import android.app.Activity;
+import android.view.Menu;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.search2.SearchFeatureProviderImpl;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.verify;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class SearchFeatureProviderImplTest {
+    private SearchFeatureProviderImpl mProvider;
+    private Activity mActivity;
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Menu menu;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mActivity = Robolectric.buildActivity(Activity.class).create().visible().get();
+        mProvider = (SearchFeatureProviderImpl) FeatureFactory.getFactory(mActivity)
+                .getSearchFeatureProvider(mActivity);
+    }
+
+    @Test
+    public void testPassNull_NoError() {
+        mProvider.setUpSearchMenu(null,null);
+    }
+
+    @Test
+    public void testSetUpMenu_HasItemAdded() {
+        mProvider.setUpSearchMenu(menu, mActivity);
+
+        verify(menu).add(anyInt(),anyInt(), anyInt(), anyString());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java
new file mode 100644 (file)
index 0000000..c2ec49c
--- /dev/null
@@ -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();
+    }
+}
+
+
index 3528863..f702cdc 100644 (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;
+    }
 }