Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)

Bug: 166295507
Merged-In: I56ccb7002a0f00000936ba02c1926fb4929ae34b
Change-Id: I9405533c2eee3323e5d1b87ca972759bceb29073
diff --git a/Android.bp b/Android.bp
index fc8062a..26c946d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22,6 +22,7 @@
     privileged: true,
     required: ["privapp_whitelist_com.android.settings.intelligence"],
 
+    libs: ["android.car-stubs"],
     static_libs: [
         "androidx.legacy_legacy-support-v4",
         "androidx.legacy_legacy-support-v13",
@@ -30,6 +31,7 @@
         "androidx.preference_preference",
         "androidx.recyclerview_recyclerview",
         "androidx.legacy_legacy-preference-v14",
+        "car-ui-lib",
     ],
     resource_dirs: ["res"],
     srcs: [
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5b0bd6a..281f5be 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -25,6 +25,8 @@
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
     <uses-permission android:name="android.permission.MANAGE_FINGERPRINT" />
 
+    <uses-sdk android:targetSdkVersion="24" />
+
     <application
         android:label="@string/app_name_settings_intelligence"
         android:icon="@mipmap/ic_launcher"
diff --git a/OWNERS b/OWNERS
index 417ec58..620981f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,8 @@
 ## People who can approve changes
 
 # Android auto
-rlagos@google.com
+rlagos@google.com # OWNER for SUW related code
+ericberglund@google.com # OWNER for Car Settings related code
 
 # TV Settings
 leifhendrik@google.com
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 4e8ae4e..c0453b1 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -21,5 +21,8 @@
     <style name="Theme.Settings.NoActionBar">
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
+        <item name="android:windowSoftInputMode">adjustPan</item>
     </style>
+
+    <style name="Theme.CarSettings" parent="@style/Theme.CarUi.WithToolbar"/>
 </resources>
\ No newline at end of file
diff --git a/res/xml/car_search_fragment.xml b/res/xml/car_search_fragment.xml
new file mode 100644
index 0000000..201c60a
--- /dev/null
+++ b/res/xml/car_search_fragment.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/app_name_settings_intelligence"/>
diff --git a/src/com/android/settings/intelligence/search/SearchActivity.java b/src/com/android/settings/intelligence/search/SearchActivity.java
index 3521ef3..229c926 100644
--- a/src/com/android/settings/intelligence/search/SearchActivity.java
+++ b/src/com/android/settings/intelligence/search/SearchActivity.java
@@ -17,28 +17,36 @@
 
 package com.android.settings.intelligence.search;
 
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentManager;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.view.WindowManager;
 
 import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.car.CarSearchFragment;
 
-public class SearchActivity extends Activity {
+public class SearchActivity extends FragmentActivity {
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        if (isAutomotive()) {
+            // Automotive relies on a different theme. Apply before calling super so that
+            // fragments are restored properly on configuration changes.
+            setTheme(R.style.Theme_CarSettings);
+        }
         super.onCreate(savedInstanceState);
         setContentView(R.layout.search_main);
-        // Keeps layouts in-place when keyboard opens.
-        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
 
-        FragmentManager fragmentManager = getFragmentManager();
+        FragmentManager fragmentManager = getSupportFragmentManager();
         Fragment fragment = fragmentManager.findFragmentById(R.id.main_content);
         if (fragment == null) {
+            fragment = isAutomotive() ?
+                    new CarSearchFragment() : new SearchFragment();
             fragmentManager.beginTransaction()
-                    .add(R.id.main_content, new SearchFragment())
+                    .add(R.id.main_content, fragment)
                     .commit();
         }
     }
@@ -48,4 +56,8 @@
         finish();
         return true;
     }
+
+    private boolean isAutomotive() {
+        return getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
 }
diff --git a/src/com/android/settings/intelligence/search/SearchCommon.java b/src/com/android/settings/intelligence/search/SearchCommon.java
new file mode 100644
index 0000000..4666837
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/SearchCommon.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search;
+
+/**
+ * Shared constant variables and identifiers.
+ */
+public class SearchCommon {
+    /**
+     * Instance state key for the currently entered query.
+     */
+    public static final String STATE_QUERY = "state_query";
+    /**
+     * Instance state key for whether or not saved queries are being shown.
+     */
+    public static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
+    /**
+     * Instance state key for whether or not a query has been entered yet.
+     */
+    public static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
+
+    /**
+     * Identifier constants for the search loaders.
+     */
+    public static final class SearchLoaderId {
+        /**
+         * Loader identifier to get search results.
+         */
+        public static final int SEARCH_RESULT = 1;
+        /**
+         * Loader identifier to save an entered query.
+         */
+        public static final int SAVE_QUERY_TASK = 2;
+        /**
+         * Loader identifier to remove an entered query.
+         */
+        public static final int REMOVE_QUERY_TASK = 3;
+        /**
+         * Loader identifier to get currently saved queries.
+         */
+        public static final int SAVED_QUERIES = 4;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/SearchFragment.java b/src/com/android/settings/intelligence/search/SearchFragment.java
index 1ace8fc..c76eda3 100644
--- a/src/com/android/settings/intelligence/search/SearchFragment.java
+++ b/src/com/android/settings/intelligence/search/SearchFragment.java
@@ -20,14 +20,14 @@
 import static com.android.settings.intelligence.nano.SettingsIntelligenceLogProto.SettingsIntelligenceEvent;
 
 import android.app.Activity;
-import android.app.Fragment;
-import android.app.LoaderManager;
 import android.content.Context;
-import android.content.Loader;
 import android.os.Bundle;
 import androidx.annotation.VisibleForTesting;
+import androidx.loader.content.Loader;
+import androidx.loader.app.LoaderManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.fragment.app.Fragment;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
@@ -64,21 +64,6 @@
         LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
     private static final String TAG = "SearchFragment";
 
-    // State values
-    private static final String STATE_QUERY = "state_query";
-    private static final String STATE_SHOWING_SAVED_QUERY = "state_showing_saved_query";
-    private static final String STATE_NEVER_ENTERED_QUERY = "state_never_entered_query";
-
-    public static final class SearchLoaderId {
-        // Search Query IDs
-        public static final int SEARCH_RESULT = 1;
-
-        // Saved Query IDs
-        public static final int SAVE_QUERY_TASK = 2;
-        public static final int REMOVE_QUERY_TASK = 3;
-        public static final int SAVED_QUERIES = 4;
-    }
-
     @VisibleForTesting
     String mQuery;
 
@@ -134,9 +119,9 @@
         mSearchFeatureProvider.initFeedbackButton();
 
         if (savedInstanceState != null) {
-            mQuery = savedInstanceState.getString(STATE_QUERY);
-            mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
-            mShowingSavedQuery = savedInstanceState.getBoolean(STATE_SHOWING_SAVED_QUERY);
+            mQuery = savedInstanceState.getString(SearchCommon.STATE_QUERY);
+            mNeverEnteredQuery = savedInstanceState.getBoolean(SearchCommon.STATE_NEVER_ENTERED_QUERY);
+            mShowingSavedQuery = savedInstanceState.getBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY);
         } else {
             mShowingSavedQuery = true;
         }
@@ -208,9 +193,9 @@
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putString(STATE_QUERY, mQuery);
-        outState.putBoolean(STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
-        outState.putBoolean(STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
+        outState.putString(SearchCommon.STATE_QUERY, mQuery);
+        outState.putBoolean(SearchCommon.STATE_NEVER_ENTERED_QUERY, mNeverEnteredQuery);
+        outState.putBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
     }
 
     @Override
@@ -238,7 +223,7 @@
 
         if (isEmptyQuery) {
             final LoaderManager loaderManager = getLoaderManager();
-            loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
+            loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT);
             mShowingSavedQuery = true;
             mSavedQueryController.loadSavedQueries();
             mSearchFeatureProvider.hideFeedbackButton(getView());
@@ -263,7 +248,7 @@
         final Activity activity = getActivity();
 
         switch (id) {
-            case SearchLoaderId.SEARCH_RESULT:
+            case SearchCommon.SearchLoaderId.SEARCH_RESULT:
                 return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
             default:
                 return null;
@@ -292,7 +277,7 @@
             mSavedQueryController.loadSavedQueries();
         } else {
             final LoaderManager loaderManager = getLoaderManager();
-            loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
+            loaderManager.initLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, null /* args */,
                     this /* callback */);
         }
 
@@ -338,8 +323,8 @@
     private void restartLoaders() {
         mShowingSavedQuery = false;
         final LoaderManager loaderManager = getLoaderManager();
-        loaderManager.restartLoader(
-                SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
+        loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,
+                null /* args */, this /* callback */);
     }
 
     public String getQuery() {
diff --git a/src/com/android/settings/intelligence/search/car/CarFeatureFactoryImpl.java b/src/com/android/settings/intelligence/search/car/CarFeatureFactoryImpl.java
new file mode 100644
index 0000000..452ab27
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarFeatureFactoryImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import androidx.annotation.Keep;
+
+import com.android.settings.intelligence.overlay.FeatureFactoryImpl;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+
+/**
+ * FeatureFactory implementation for car settings search.
+ */
+@Keep
+public class CarFeatureFactoryImpl extends FeatureFactoryImpl {
+    @Override
+    public SearchFeatureProvider searchFeatureProvider() {
+        if (mSearchFeatureProvider == null) {
+            mSearchFeatureProvider = new CarSearchFeatureProviderImpl();
+        }
+        return mSearchFeatureProvider;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/car/CarIntentSearchViewHolder.java b/src/com/android/settings/intelligence/search/car/CarIntentSearchViewHolder.java
new file mode 100644
index 0000000..bc3ef60
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarIntentSearchViewHolder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.AppSearchResult;
+import com.android.settings.intelligence.search.SearchResult;
+
+import java.util.List;
+
+/**
+ * ViewHolder for intent based search results.
+ */
+public class CarIntentSearchViewHolder extends CarSearchViewHolder {
+    private static final String TAG = "CarIntentSearchViewHolder";
+    private static final int REQUEST_CODE_NO_OP = 0;
+
+    public CarIntentSearchViewHolder(View view) {
+        super(view);
+    }
+
+    @Override
+    public void onBind(CarSearchFragment fragment, SearchResult result) {
+        mTitle.setText(result.title);
+        if (result instanceof AppSearchResult) {
+            AppSearchResult appResult = (AppSearchResult) result;
+            PackageManager pm = fragment.getActivity().getPackageManager();
+            mIcon.setImageDrawable(appResult.info.loadIcon(pm));
+        } else {
+            mIcon.setImageDrawable(result.icon);
+        }
+        bindBreadcrumbView(result);
+
+        itemView.setOnClickListener(v -> {
+            fragment.onSearchResultClicked(/* resultViewHolder= */ this, result);
+            Intent intent = result.payload.getIntent();
+            if (result instanceof AppSearchResult) {
+                AppSearchResult appResult = (AppSearchResult) result;
+                fragment.getActivity().startActivity(intent);
+            } else {
+                PackageManager pm = fragment.getActivity().getPackageManager();
+                List<ResolveInfo> info = pm.queryIntentActivities(intent, /* flags= */ 0);
+                if (info != null && !info.isEmpty()) {
+                    fragment.startActivityForResult(intent, REQUEST_CODE_NO_OP);
+                } else {
+                    Log.e(TAG, "Cannot launch search result, title: "
+                            + result.title + ", " + intent);
+                }
+            }
+        });
+    }
+
+    private void bindBreadcrumbView(SearchResult result) {
+        if (result.breadcrumbs == null || result.breadcrumbs.isEmpty()) {
+            mSummary.setVisibility(View.GONE);
+            return;
+        }
+        String breadcrumb = result.breadcrumbs.get(0);
+        int count = result.breadcrumbs.size();
+        for (int i = 1; i < count; i++) {
+            breadcrumb = mContext.getString(R.string.search_breadcrumb_connector,
+                    breadcrumb, result.breadcrumbs.get(i));
+        }
+        if (breadcrumb == null || TextUtils.isEmpty(breadcrumb.trim())) {
+            mSummary.setVisibility(View.GONE);
+        } else {
+            mSummary.setText(breadcrumb);
+            mSummary.setVisibility(View.VISIBLE);
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/car/CarSearchFeatureProviderImpl.java b/src/com/android/settings/intelligence/search/car/CarSearchFeatureProviderImpl.java
new file mode 100644
index 0000000..2491cbd
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarSearchFeatureProviderImpl.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchFragment;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchResultLoader;
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexData;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.indexing.car.CarDatabaseIndexingManager;
+import com.android.settings.intelligence.search.query.DatabaseResultTask;
+import com.android.settings.intelligence.search.query.InstalledAppResultTask;
+import com.android.settings.intelligence.search.query.SearchQueryTask;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryLoader;
+import com.android.settings.intelligence.search.sitemap.SiteMapManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+/**
+ * SearchFeatureProvider for car settings search.
+ */
+public class CarSearchFeatureProviderImpl implements SearchFeatureProvider {
+    private static final String TAG = "CarSearchFeatureProvider";
+    private static final long SMART_SEARCH_RANKING_TIMEOUT = 300L;
+
+    private CarDatabaseIndexingManager mDatabaseIndexingManager;
+    private ExecutorService mExecutorService;
+    private SiteMapManager mSiteMapManager;
+
+    @Override
+    public SearchResultLoader getSearchResultLoader(Context context, String query) {
+        return new SearchResultLoader(context, cleanQuery(query));
+    }
+
+    @Override
+    public List<SearchQueryTask> getSearchQueryTasks(Context context, String query) {
+        List<SearchQueryTask> tasks = new ArrayList<>();
+        String cleanQuery = cleanQuery(query);
+        tasks.add(DatabaseResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        tasks.add(InstalledAppResultTask.newTask(context, getSiteMapManager(), cleanQuery));
+        return tasks;
+    }
+
+    @Override
+    public SavedQueryLoader getSavedQueryLoader(Context context) {
+        return new SavedQueryLoader(context);
+    }
+
+    @Override
+    public DatabaseIndexingManager getIndexingManager(Context context) {
+        if (mDatabaseIndexingManager == null) {
+            mDatabaseIndexingManager = new CarDatabaseIndexingManager(
+                    context.getApplicationContext());
+        }
+        return mDatabaseIndexingManager;
+    }
+
+    @Override
+    public SiteMapManager getSiteMapManager() {
+        if (mSiteMapManager == null) {
+            mSiteMapManager = new SiteMapManager();
+        }
+        return mSiteMapManager;
+    }
+
+    @Override
+    public boolean isIndexingComplete(Context context) {
+        return getIndexingManager(context).isIndexingComplete();
+    }
+
+    @Override
+    public void initFeedbackButton() {
+    }
+
+    @Override
+    public void showFeedbackButton(SearchFragment fragment, View root) {
+    }
+
+    @Override
+    public void hideFeedbackButton(View root) {
+    }
+
+    @Override
+    public void searchResultClicked(Context context, String query, SearchResult searchResult) {
+    }
+
+    @Override
+    public boolean isSmartSearchRankingEnabled(Context context) {
+        return false;
+    }
+
+    @Override
+    public long smartSearchRankingTimeoutMs(Context context) {
+        return SMART_SEARCH_RANKING_TIMEOUT;
+    }
+
+    @Override
+    public void searchRankingWarmup(Context context) {
+    }
+
+    @Override
+    public FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query) {
+        return null;
+    }
+
+    @Override
+    public void updateIndexAsync(Context context, IndexingCallback callback) {
+        if (DEBUG) {
+            Log.d(TAG, "updating index async");
+        }
+        getIndexingManager(context).indexDatabase(callback);
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        if (mExecutorService == null) {
+            mExecutorService = Executors.newCachedThreadPool();
+        }
+        return mExecutorService;
+    }
+
+    /**
+     * A generic method to make the query suitable for searching the database.
+     *
+     * @return the cleaned query string
+     */
+    private String cleanQuery(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+        if (Locale.getDefault().equals(Locale.JAPAN)) {
+            query = IndexData.normalizeJapaneseString(query);
+        }
+        return query.trim();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/car/CarSearchFragment.java b/src/com/android/settings/intelligence/search/car/CarSearchFragment.java
new file mode 100644
index 0000000..0c49b11
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarSearchFragment.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import static com.android.car.ui.core.CarUi.requireInsets;
+import static com.android.car.ui.core.CarUi.requireToolbar;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.NonNull;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.preference.PreferenceFragment;
+import com.android.car.ui.toolbar.MenuItem;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchCommon;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.indexing.IndexingCallback;
+import com.android.settings.intelligence.search.savedqueries.car.CarSavedQueryController;
+import com.android.settings.intelligence.search.savedqueries.car.CarSavedQueryViewHolder;
+
+import java.util.List;
+
+/**
+ * Search fragment for car settings.
+ */
+public class CarSearchFragment extends PreferenceFragment implements
+        LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
+
+    private SearchFeatureProvider mSearchFeatureProvider;
+
+    private ToolbarController mToolbar;
+    private RecyclerView mRecyclerView;
+
+    private String mQuery;
+    private boolean mShowingSavedQuery;
+
+    private CarSearchResultsAdapter mSearchAdapter;
+    private CarSavedQueryController mSavedQueryController;
+
+    private final RecyclerView.OnScrollListener mScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                    if (dy != 0) {
+                        hideKeyboard();
+                    }
+                }
+            };
+
+    @Override
+    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+        setPreferencesFromResource(R.xml.car_search_fragment, rootKey);
+    }
+
+    protected ToolbarController getToolbar() {
+        return requireToolbar(requireActivity());
+    }
+
+    protected List<MenuItem> getToolbarMenuItems() {
+        return null;
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            mQuery = savedInstanceState.getString(SearchCommon.STATE_QUERY);
+            mShowingSavedQuery = savedInstanceState.getBoolean(
+                    SearchCommon.STATE_SHOWING_SAVED_QUERY);
+        } else {
+            mShowingSavedQuery = true;
+        }
+
+        LoaderManager loaderManager = getLoaderManager();
+        mSearchAdapter = new CarSearchResultsAdapter(/* fragment= */ this);
+        mSavedQueryController = new CarSavedQueryController(
+                getContext(), loaderManager, mSearchAdapter);
+        mSearchFeatureProvider.updateIndexAsync(getContext(), /* indexingCallback= */ this);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mToolbar = getToolbar();
+        if (mToolbar != null) {
+            List<MenuItem> items = getToolbarMenuItems();
+            mToolbar.setTitle(getPreferenceScreen().getTitle());
+            mToolbar.setMenuItems(items);
+            mToolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
+            mToolbar.setState(Toolbar.State.SUBPAGE);
+            mToolbar.setState(Toolbar.State.SEARCH);
+            mToolbar.setSearchHint(R.string.abc_search_hint);
+            mToolbar.registerOnSearchListener(this::onQueryTextChange);
+            mToolbar.registerOnSearchCompletedListener(this::onSearchComplete);
+            mToolbar.setShowMenuItemsWhileSearching(true);
+            mToolbar.setSearchQuery(mQuery);
+        }
+        mRecyclerView = getListView();
+        if (mRecyclerView != null) {
+            mRecyclerView.setAdapter(mSearchAdapter);
+            mRecyclerView.addOnScrollListener(mScrollListener);
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        onCarUiInsetsChanged(requireInsets(requireActivity()));
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putString(SearchCommon.STATE_QUERY, mQuery);
+        outState.putBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery);
+    }
+
+    private void onQueryTextChange(String query) {
+        if (TextUtils.equals(query, mQuery)) {
+            return;
+        }
+        boolean isEmptyQuery = TextUtils.isEmpty(query);
+
+        mQuery = query;
+
+        // If indexing is not finished, register the query text, but don't search.
+        if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
+            mToolbar.getProgressBar().setVisible(!isEmptyQuery);
+            return;
+        }
+
+        if (isEmptyQuery) {
+            LoaderManager loaderManager = getLoaderManager();
+            loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT);
+            mShowingSavedQuery = true;
+            mSavedQueryController.loadSavedQueries();
+        } else {
+            restartLoaders();
+        }
+    }
+
+    private void onSearchComplete() {
+        if (!TextUtils.isEmpty(mQuery)) {
+            mSavedQueryController.saveQuery(mQuery);
+        }
+    }
+
+    /**
+     * Gets called when a saved query is clicked.
+     */
+    public void onSavedQueryClicked(CarSavedQueryViewHolder vh, CharSequence query) {
+        String queryString = query.toString();
+        mToolbar.setSearchQuery(queryString);
+        onQueryTextChange(queryString);
+    }
+
+    /**
+     * Gets called when a search result is clicked.
+     */
+    public void onSearchResultClicked(CarSearchViewHolder resultViewHolder, SearchResult result) {
+        mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result);
+        mSavedQueryController.saveQuery(mQuery);
+    }
+
+    @Override
+    public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
+        Activity activity = getActivity();
+
+        if (id == SearchCommon.SearchLoaderId.SEARCH_RESULT) {
+            return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
+        }
+        return null;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
+            List<? extends SearchResult> data) {
+        mSearchAdapter.postSearchResults(data);
+        mRecyclerView.scrollToPosition(0);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
+    }
+
+    /**
+     * Gets called when Indexing is completed.
+     */
+    @Override
+    public void onIndexingFinished() {
+        if (getActivity() == null) {
+            return;
+        }
+        mToolbar.getProgressBar().setVisible(false);
+        if (mShowingSavedQuery) {
+            mSavedQueryController.loadSavedQueries();
+        } else {
+            LoaderManager loaderManager = getLoaderManager();
+            loaderManager.initLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,
+                    /* args= */ null, /* callback= */ this);
+        }
+        requery();
+    }
+
+    private void requery() {
+        if (TextUtils.isEmpty(mQuery)) {
+            return;
+        }
+        String query = mQuery;
+        mQuery = "";
+        onQueryTextChange(query);
+    }
+
+    private void restartLoaders() {
+        mShowingSavedQuery = false;
+        LoaderManager loaderManager = getLoaderManager();
+        loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,
+                /* args= */ null, /* callback= */ this);
+    }
+
+    private void hideKeyboard() {
+        Activity activity = getActivity();
+        if (activity != null) {
+            View view = activity.getCurrentFocus();
+            InputMethodManager imm = (InputMethodManager)
+                    activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+            if (imm.isActive(view)) {
+                imm.hideSoftInputFromWindow(view.getWindowToken(), /* flags= */ 0);
+            }
+        }
+
+        if (mRecyclerView != null) {
+            mRecyclerView.requestFocus();
+        }
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/car/CarSearchResultsAdapter.java b/src/com/android/settings/intelligence/search/car/CarSearchResultsAdapter.java
new file mode 100644
index 0000000..be35482
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarSearchResultsAdapter.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.ResultPayload;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.SearchResultDiffCallback;
+import com.android.settings.intelligence.search.savedqueries.car.CarSavedQueryViewHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RecyclerView Adapter for the car search results RecyclerView.
+ * The adapter uses the CarSearchViewHolder for its view contents.
+ */
+public class CarSearchResultsAdapter extends RecyclerView.Adapter<CarSearchViewHolder> {
+
+    private final CarSearchFragment mFragment;
+    private final List<SearchResult> mSearchResults;
+
+    public CarSearchResultsAdapter(CarSearchFragment fragment) {
+        mFragment = fragment;
+        mSearchResults = new ArrayList<>();
+
+        setHasStableIds(true);
+    }
+
+    @Override
+    public CarSearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        Context context = parent.getContext();
+        LayoutInflater inflater = LayoutInflater.from(context);
+        View view;
+        switch (viewType) {
+            case ResultPayload.PayloadType.INTENT:
+                view = inflater.inflate(R.layout.car_ui_preference, parent,
+                        /* attachToRoot= */ false);
+                return new CarIntentSearchViewHolder(view);
+            case ResultPayload.PayloadType.SAVED_QUERY:
+                view = inflater.inflate(R.layout.car_ui_preference, parent,
+                        /* attachToRoot= */ false);
+                return new CarSavedQueryViewHolder(view);
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(CarSearchViewHolder holder, int position) {
+        holder.onBind(mFragment, mSearchResults.get(position));
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return mSearchResults.get(position).hashCode();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return mSearchResults.get(position).viewType;
+    }
+
+    @Override
+    public int getItemCount() {
+        return mSearchResults.size();
+    }
+
+    protected void postSearchResults(List<? extends SearchResult> newSearchResults) {
+        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+                new SearchResultDiffCallback(mSearchResults, newSearchResults));
+        mSearchResults.clear();
+        mSearchResults.addAll(newSearchResults);
+        diffResult.dispatchUpdatesTo(/* adapter= */ this);
+    }
+
+    /**
+     * Displays recent searched queries.
+     */
+    public void displaySavedQuery(List<? extends SearchResult> data) {
+        clearResults();
+        mSearchResults.addAll(data);
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Clear current search results.
+     */
+    public void clearResults() {
+        mSearchResults.clear();
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Get current search results.
+     */
+    public List<SearchResult> getSearchResults() {
+        return mSearchResults;
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/car/CarSearchViewHolder.java b/src/com/android/settings/intelligence/search/car/CarSearchViewHolder.java
new file mode 100644
index 0000000..2657d9a
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/car/CarSearchViewHolder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.car;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.intelligence.search.SearchResult;
+
+/** The ViewHolder for the car search RecyclerView.
+ * There are multiple search result types with different UI requirements, such as Intent results
+ * and saved query results.
+ */
+public abstract class CarSearchViewHolder extends RecyclerView.ViewHolder {
+    protected Context mContext;
+    protected ImageView mIcon;
+    protected TextView mTitle;
+    protected TextView mSummary;
+
+    public CarSearchViewHolder(View view) {
+        super(view);
+        mContext = view.getContext();
+        mIcon = view.findViewById(android.R.id.icon);
+        mTitle = view.findViewById(android.R.id.title);
+        mSummary = view.findViewById(android.R.id.summary);
+    }
+
+    /**
+     * Update the ViewHolder data when bound.
+     */
+    public abstract void onBind(CarSearchFragment fragment, SearchResult result);
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
index 0053f14..32add54 100644
--- a/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
+++ b/src/com/android/settings/intelligence/search/indexing/DatabaseIndexingManager.java
@@ -178,7 +178,7 @@
 
     private List<IndexData> getIndexData(PreIndexData data) {
         if (mConverter == null) {
-            mConverter = new IndexDataConverter(mContext);
+            mConverter = getIndexDataConverter(mContext);
         }
         return mConverter.convertPreIndexDataToIndexData(data);
     }
@@ -186,7 +186,7 @@
     private List<SiteMapPair> getSiteMapPairs(List<IndexData> indexData,
             List<Pair<String, String>> siteMapClassNames) {
         if (mConverter == null) {
-            mConverter = new IndexDataConverter(mContext);
+            mConverter = getIndexDataConverter(mContext);
         }
         return mConverter.convertSiteMapPairs(indexData, siteMapClassNames);
     }
@@ -312,6 +312,14 @@
         }
     }
 
+    /**
+     * Protected method to get a new IndexDataConverter instance. This method can be overridden
+     * in subclasses to substitute in a custom IndexDataConverter.
+     */
+    protected IndexDataConverter getIndexDataConverter(Context context) {
+        return new IndexDataConverter(context);
+    }
+
     public class IndexingTask extends AsyncTask<Void, Void, Void> {
 
         @VisibleForTesting
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexData.java b/src/com/android/settings/intelligence/search/indexing/IndexData.java
index 904deaf..7318eaa 100644
--- a/src/com/android/settings/intelligence/search/indexing/IndexData.java
+++ b/src/com/android/settings/intelligence/search/indexing/IndexData.java
@@ -68,7 +68,7 @@
     private static final Pattern REMOVE_DIACRITICALS_PATTERN
             = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
 
-    private IndexData(Builder builder) {
+    protected IndexData(Builder builder) {
         locale = Locale.getDefault().toString();
         updatedTitle = normalizeHyphen(builder.mTitle);
         updatedSummaryOn = normalizeHyphen(builder.mSummaryOn);
@@ -186,6 +186,18 @@
             return mKey;
         }
 
+        public String getIntentAction() {
+            return mIntentAction;
+        }
+
+        public String getIntentTargetPackage() {
+            return mIntentTargetPackage;
+        }
+
+        public String getIntentTargetClass() {
+            return mIntentTargetClass;
+        }
+
         public Builder setSummaryOn(String summaryOn) {
             mSummaryOn = summaryOn;
             return this;
@@ -290,9 +302,10 @@
         }
 
         /**
-         * Adds Intent payload to builder.
+         * Builds Intent payload for the builder.
+         * This protected method that can be overridden in a subclass for custom intents.
          */
-        private Intent buildIntent(Context context) {
+        protected Intent buildIntent(Context context) {
             final Intent intent;
 
             // TODO REFACTOR (b/62807132) With inline results re-add proper intent support
diff --git a/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
index bd53596..f57ce6b 100644
--- a/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
+++ b/src/com/android/settings/intelligence/search/indexing/IndexDataConverter.java
@@ -165,7 +165,7 @@
         // A row is enabled if it does not show up as an nonIndexableKey
         boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key));
 
-        final IndexData.Builder builder = new IndexData.Builder();
+        final IndexData.Builder builder = getIndexDataBuilder();
         builder.setTitle(raw.title)
                 .setSummaryOn(raw.summaryOn)
                 .setEntries(raw.entries)
@@ -244,7 +244,7 @@
             headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
             enabled = !nonIndexableKeys.contains(headerKey);
             // TODO: Set payload type for header results
-            IndexData.Builder headerBuilder = new IndexData.Builder();
+            IndexData.Builder headerBuilder = getIndexDataBuilder();
             headerBuilder.setTitle(headerTitle)
                     .setSummaryOn(headerSummary)
                     .setScreenTitle(screenTitle)
@@ -286,7 +286,7 @@
                     isHeaderUnique = false;
                 }
 
-                builder = new IndexData.Builder();
+                builder = getIndexDataBuilder();
                 builder.setTitle(title)
                         .setKeywords(keywords)
                         .setClassName(sir.className)
@@ -365,4 +365,8 @@
         final Set<String> result = nonIndexableKeys.get(authority);
         return result != null ? result : new ArraySet<>();
     }
+
+    protected IndexData.Builder getIndexDataBuilder() {
+        return new IndexData.Builder();
+    }
 }
diff --git a/src/com/android/settings/intelligence/search/indexing/car/CarDatabaseIndexingManager.java b/src/com/android/settings/intelligence/search/indexing/car/CarDatabaseIndexingManager.java
new file mode 100644
index 0000000..3026e82
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/car/CarDatabaseIndexingManager.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.indexing.car;
+
+import android.content.Context;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingManager;
+import com.android.settings.intelligence.search.indexing.IndexDataConverter;
+import com.android.settings.intelligence.search.indexing.PreIndexData;
+
+/**
+ * Car extension to {@link DatabaseIndexingManager} to use {@link CarIndexDataConverter} for
+ * converting {@link PreIndexData} into {@link CarIndexData}.
+ */
+public class CarDatabaseIndexingManager extends DatabaseIndexingManager {
+
+    public CarDatabaseIndexingManager(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected IndexDataConverter getIndexDataConverter(Context context) {
+        return new CarIndexDataConverter(context);
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/indexing/car/CarIndexData.java b/src/com/android/settings/intelligence/search/indexing/car/CarIndexData.java
new file mode 100644
index 0000000..a57d003
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/car/CarIndexData.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.indexing.car;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
+import com.android.settings.intelligence.search.indexing.IndexData;
+
+/**
+ * Car data class representing a single row in the Setting Search results database.
+ */
+public class CarIndexData extends IndexData {
+
+    public CarIndexData(IndexData.Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Builder class for {@link CarIndexData}, extending {@link IndexData.Builder}, which replaces
+     * all intents with direct search intents, since CarSettings doesn't support
+     * SearchResultTrampolineIntents.
+     */
+    public static class Builder extends IndexData.Builder {
+        @Override
+        protected Intent buildIntent(Context context) {
+            return DatabaseIndexingUtils.buildDirectSearchResultIntent(getIntentAction(),
+                    getIntentTargetPackage(), getIntentTargetClass(), getKey());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/indexing/car/CarIndexDataConverter.java b/src/com/android/settings/intelligence/search/indexing/car/CarIndexDataConverter.java
new file mode 100644
index 0000000..11f35cb
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/indexing/car/CarIndexDataConverter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.indexing.car;
+
+import android.content.Context;
+
+import com.android.settings.intelligence.search.indexing.IndexData;
+import com.android.settings.intelligence.search.indexing.IndexDataConverter;
+import com.android.settings.intelligence.search.indexing.PreIndexData;
+
+/**
+ * Car helper class to convert {@link PreIndexData} to {@link CarIndexData}.
+ */
+public class CarIndexDataConverter extends IndexDataConverter {
+
+    public CarIndexDataConverter(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected IndexData.Builder getIndexDataBuilder() {
+        return new CarIndexData.Builder();
+    }
+}
diff --git a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
index e4c2cc1..95dfde4 100644
--- a/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
+++ b/src/com/android/settings/intelligence/search/savedqueries/SavedQueryController.java
@@ -16,18 +16,18 @@
 
 package com.android.settings.intelligence.search.savedqueries;
 
-import android.app.LoaderManager;
 import android.content.Context;
-import android.content.Loader;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
 
 import com.android.settings.intelligence.R;
 import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchCommon;
 import com.android.settings.intelligence.search.SearchFeatureProvider;
-import com.android.settings.intelligence.search.SearchFragment;
 import com.android.settings.intelligence.search.SearchResult;
 import com.android.settings.intelligence.search.SearchResultsAdapter;
 
@@ -59,11 +59,11 @@
     @Override
     public Loader onCreateLoader(int id, Bundle args) {
         switch (id) {
-            case SearchFragment.SearchLoaderId.SAVE_QUERY_TASK:
+            case SearchCommon.SearchLoaderId.SAVE_QUERY_TASK:
                 return new SavedQueryRecorder(mContext, args.getString(ARG_QUERY));
-            case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
+            case SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK:
                 return new SavedQueryRemover(mContext);
-            case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+            case SearchCommon.SearchLoaderId.SAVED_QUERIES:
                 return mSearchFeatureProvider.getSavedQueryLoader(mContext);
         }
         return null;
@@ -72,11 +72,11 @@
     @Override
     public void onLoadFinished(Loader loader, Object data) {
         switch (loader.getId()) {
-            case SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK:
-                mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES,
+            case SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK:
+                mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVED_QUERIES,
                         null /* args */, this /* callback */);
                 break;
-            case SearchFragment.SearchLoaderId.SAVED_QUERIES:
+            case SearchCommon.SearchLoaderId.SAVED_QUERIES:
                 if (SearchFeatureProvider.DEBUG) {
                     Log.d(TAG, "Saved queries loaded");
                 }
@@ -107,7 +107,7 @@
     public void saveQuery(String query) {
         final Bundle args = new Bundle();
         args.putString(ARG_QUERY, query);
-        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVE_QUERY_TASK, args,
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVE_QUERY_TASK, args,
                 this /* callback */);
     }
 
@@ -116,7 +116,7 @@
      */
     public void removeQueries() {
         final Bundle args = new Bundle();
-        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.REMOVE_QUERY_TASK, args,
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK, args,
                 this /* callback */);
     }
 
@@ -124,7 +124,7 @@
         if (SearchFeatureProvider.DEBUG) {
             Log.d(TAG, "loading saved queries");
         }
-        mLoaderManager.restartLoader(SearchFragment.SearchLoaderId.SAVED_QUERIES, null /* args */,
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVED_QUERIES, null /* args */,
                 this /* callback */);
     }
 }
diff --git a/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryController.java b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryController.java
new file mode 100644
index 0000000..fa222a0
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryController.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.savedqueries.car;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+
+import com.android.settings.intelligence.overlay.FeatureFactory;
+import com.android.settings.intelligence.search.SearchCommon;
+import com.android.settings.intelligence.search.SearchFeatureProvider;
+import com.android.settings.intelligence.search.SearchResult;
+import com.android.settings.intelligence.search.car.CarSearchResultsAdapter;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryRecorder;
+import com.android.settings.intelligence.search.savedqueries.SavedQueryRemover;
+
+import java.util.List;
+
+/**
+ * Helper class for managing saved queries.
+ */
+public class CarSavedQueryController implements LoaderManager.LoaderCallbacks,
+        MenuItem.OnMenuItemClickListener {
+
+    private static final String ARG_QUERY = "remove_query";
+    private static final String TAG = "CarSearchSavedQueryCtrl";
+
+    private static final int MENU_SEARCH_HISTORY = 1000;
+
+    private final Context mContext;
+    private final LoaderManager mLoaderManager;
+    private final SearchFeatureProvider mSearchFeatureProvider;
+    private final CarSearchResultsAdapter mResultAdapter;
+
+    public CarSavedQueryController(Context context, LoaderManager loaderManager,
+            CarSearchResultsAdapter resultsAdapter) {
+        mContext = context;
+        mLoaderManager = loaderManager;
+        mResultAdapter = resultsAdapter;
+        mSearchFeatureProvider = FeatureFactory.get(context)
+                .searchFeatureProvider();
+    }
+
+    @Override
+    public Loader onCreateLoader(int id, Bundle args) {
+        switch (id) {
+            case SearchCommon.SearchLoaderId.SAVE_QUERY_TASK:
+                return new SavedQueryRecorder(mContext, args.getString(ARG_QUERY));
+            case SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK:
+                return new SavedQueryRemover(mContext);
+            case SearchCommon.SearchLoaderId.SAVED_QUERIES:
+                return mSearchFeatureProvider.getSavedQueryLoader(mContext);
+        }
+        return null;
+    }
+
+    @Override
+    public void onLoadFinished(Loader loader, Object data) {
+        switch (loader.getId()) {
+            case SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK:
+                mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVED_QUERIES,
+                        /* args= */ null, /* callback= */ this);
+                break;
+            case SearchCommon.SearchLoaderId.SAVED_QUERIES:
+                if (SearchFeatureProvider.DEBUG) {
+                    Log.d(TAG, "Saved queries loaded");
+                }
+                mResultAdapter.displaySavedQuery((List<SearchResult>) data);
+                break;
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader loader) {
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (item.getItemId() != MENU_SEARCH_HISTORY) {
+            return false;
+        }
+        removeQueries();
+        return true;
+    }
+
+    /**
+     * Save a query to the DB.
+     */
+    public void saveQuery(String query) {
+        Bundle args = new Bundle();
+        args.putString(ARG_QUERY, query);
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVE_QUERY_TASK, args,
+                /* callback= */ this);
+    }
+
+    /**
+     * Remove all saved queries from the DB.
+     */
+    public void removeQueries() {
+        Bundle args = new Bundle();
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.REMOVE_QUERY_TASK, args,
+                /* callback= */ this);
+    }
+
+    /**
+     * Load the saved queries from the DB.
+     */
+    public void loadSavedQueries() {
+        if (SearchFeatureProvider.DEBUG) {
+            Log.d(TAG, "loading saved queries");
+        }
+        mLoaderManager.restartLoader(SearchCommon.SearchLoaderId.SAVED_QUERIES,
+                /* args= */ null, /* callback= */ this);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryViewHolder.java b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryViewHolder.java
new file mode 100644
index 0000000..0f03c23
--- /dev/null
+++ b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryViewHolder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.intelligence.search.savedqueries.car;
+
+import android.view.View;
+
+import com.android.settings.intelligence.R;
+import com.android.settings.intelligence.search.car.CarSearchFragment;
+import com.android.settings.intelligence.search.car.CarSearchViewHolder;
+import com.android.settings.intelligence.search.SearchResult;
+
+/**
+ * ViewHolder for saved queries from past searches.
+ */
+public class CarSavedQueryViewHolder extends CarSearchViewHolder {
+
+    public CarSavedQueryViewHolder(View view) {
+        super(view);
+    }
+
+    @Override
+    public void onBind(CarSearchFragment fragment, SearchResult result) {
+        mTitle.setText(result.title);
+        mIcon.setImageResource(R.drawable.ic_restore);
+        mSummary.setVisibility(View.GONE);
+        itemView.setOnClickListener(v -> {
+            fragment.onSavedQueryClicked(CarSavedQueryViewHolder.this, result.title);
+        });
+    }
+}
diff --git a/src/com/android/settings/intelligence/utils/AsyncLoader.java b/src/com/android/settings/intelligence/utils/AsyncLoader.java
index 54b62b6..edf311e 100644
--- a/src/com/android/settings/intelligence/utils/AsyncLoader.java
+++ b/src/com/android/settings/intelligence/utils/AsyncLoader.java
@@ -1,6 +1,6 @@
 package com.android.settings.intelligence.utils;
 
-import android.content.AsyncTaskLoader;
+import androidx.loader.content.AsyncTaskLoader;
 import android.content.Context;
 
 /**