diff options
author | Alex Stetson <alexstetson@google.com> | 2021-02-23 14:38:21 -0800 |
---|---|---|
committer | Alex Stetson <alexstetson@google.com> | 2021-02-25 10:58:11 -0800 |
commit | 115b7829316d75ba4eb2e07f35c3dd2be4006726 (patch) | |
tree | fe8f826cd4e8bb057f7a4c695275054090125e67 | |
parent | 0be74c0cc29371e596372271a9c66d0347cee22b (diff) | |
download | SettingsIntelligence-115b7829316d75ba4eb2e07f35c3dd2be4006726.tar.gz |
Move car code to CarSettingsIntelligence
Migrate car-specific code from phone SettingsIntelligence to
CarSettingsIntelligence
Bug: 174688736
Test: manual
Change-Id: I26ed08ee9e0fe32161c899e390e1fd5c50f6d33d
18 files changed, 1298 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..ceef57f --- /dev/null +++ b/Android.bp @@ -0,0 +1,33 @@ +// Copyright (C) 2021 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. + +android_app { + name: "CarSettingsIntelligence", + overrides: ["SettingsIntelligence"], + optimize: { + proguard_flags_files: ["proguard.cfg"], + }, + sdk_version: "system_current", + product_specific: true, + privileged: true, + required: ["privapp_whitelist_com.android.settings.intelligence"], + + libs: ["android.car-stubs"], + static_libs: [ + "SettingsIntelligence-core", + "car-ui-lib", + ], + srcs: ["src/**/*.java"], + resource_dirs: ["res"], +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..9b0b89a --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2017 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settings.intelligence" + xmlns:tools="http://schemas.android.com/tools"> + + <application tools:node="merge"> + <activity + android:name=".search.SearchActivity" + android:theme="@style/Theme.CarSettings" + tools:replace="android:theme"/> + </application> +</manifest> @@ -0,0 +1,8 @@ +# People who can approve changes for submission. + +# Primary +alexstetson@google.com + +# Secondary (only if people in Primary are unreachable) +hseog@google.com +nehah@google.com diff --git a/proguard.cfg b/proguard.cfg new file mode 100644 index 0000000..4ff3790 --- /dev/null +++ b/proguard.cfg @@ -0,0 +1,32 @@ +# This is a configuration file for ProGuard. +# http://proguard.sourceforge.net/index.html#manual/usage.html + +# We want to keep methods in Activity that could be used in the XML attribute onClick. +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); + public void *(android.view.MenuItem); +} + +# Keep setters in Views so that animations can still work. +-keep public class * extends android.view.View { + public <init>(android.content.Context); + public <init>(android.content.Context, android.util.AttributeSet); + public <init>(android.content.Context, android.util.AttributeSet, int); + + void set*(***); + *** get*(); +} + +# Keep classes that may be inflated from XML. +-keepclasseswithmembers class * { + public <init>(android.content.Context, android.util.AttributeSet); +} +-keepclasseswithmembers class * { + public <init>(android.content.Context, android.util.AttributeSet, int); +} + +# Keep annotated classes or class members. +-keep @androidx.annotation.Keep class * +-keepclassmembers class * { + @androidx.annotation.Keep *; +} diff --git a/res/values/themes.xml b/res/values/themes.xml new file mode 100644 index 0000000..45ee9f3 --- /dev/null +++ b/res/values/themes.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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. + --> + +<resources> + <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 new file mode 100644 index 0000000..07422e2 --- /dev/null +++ b/src/com/android/settings/intelligence/search/SearchActivity.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 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; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import com.android.settings.intelligence.R; +import com.android.settings.intelligence.search.car.CarSearchFragment; + +public class SearchActivity extends FragmentActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.search_main); + + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.main_content); + if (fragment == null) { + fragmentManager.beginTransaction() + .add(R.id.main_content, new CarSearchFragment()) + .commit(); + } + } + + @Override + public boolean onNavigateUp() { + finish(); + return true; + } +} 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..a8353cd --- /dev/null +++ b/src/com/android/settings/intelligence/search/car/CarIntentSearchViewHolder.java @@ -0,0 +1,69 @@ +/* + * 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.pm.PackageManager; +import android.text.TextUtils; +import android.view.View; + +import com.android.settings.intelligence.R; +import com.android.settings.intelligence.search.AppSearchResult; +import com.android.settings.intelligence.search.SearchResult; + +/** + * ViewHolder for intent based search results. + */ +public class CarIntentSearchViewHolder extends CarSearchViewHolder { + + 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(result)); + } + + 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..702be42 --- /dev/null +++ b/src/com/android/settings/intelligence/search/car/CarSearchFragment.java @@ -0,0 +1,338 @@ +/* + * 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 static com.android.car.ui.utils.CarUiUtils.drawableToBitmap; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +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.imewidescreen.CarUiImeSearchListItem; +import com.android.car.ui.preference.PreferenceFragment; +import com.android.car.ui.recyclerview.CarUiContentListItem; +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.AppSearchResult; +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.ArrayList; +import java.util.List; + +/** + * Search fragment for car settings. + */ +public class CarSearchFragment extends PreferenceFragment implements + LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback { + private static final String TAG = "CarSearchFragment"; + private static final int REQUEST_CODE_NO_OP = 0; + + 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); + mToolbar = getToolbar(); + mSavedQueryController = new CarSavedQueryController( + getContext(), loaderManager, mSearchAdapter, mToolbar, this); + mSearchFeatureProvider.updateIndexAsync(getContext(), /* indexingCallback= */ this); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + 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(CharSequence query) { + String queryString = query.toString(); + mToolbar.setSearchQuery(queryString); + onQueryTextChange(queryString); + } + + @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) { + + if (mToolbar.canShowSearchResultItems()) { + List<CarUiImeSearchListItem> searchItems = new ArrayList<>(); + for (SearchResult result : data) { + CarUiImeSearchListItem item = new CarUiImeSearchListItem( + CarUiContentListItem.Action.ICON); + item.setTitle(result.title); + if (result.breadcrumbs != null && !result.breadcrumbs.isEmpty()) { + item.setBody(getBreadcrumb(result)); + } + + if (result instanceof AppSearchResult) { + AppSearchResult appResult = (AppSearchResult) result; + PackageManager pm = getActivity().getPackageManager(); + Drawable drawable = appResult.info.loadIcon(pm); + Bitmap bm = drawableToBitmap(drawable); + BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); + item.setIcon(bitmapDrawable); + } else if (result.icon != null) { + Bitmap bm = drawableToBitmap(result.icon); + BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); + item.setIcon(bitmapDrawable); + } + item.setOnItemClickedListener(v -> onSearchResultClicked(result)); + + searchItems.add(item); + } + mToolbar.setSearchResultItems(searchItems); + } + + mSearchAdapter.postSearchResults(data); + mRecyclerView.scrollToPosition(0); + } + + private String getBreadcrumb(SearchResult result) { + String breadcrumb = result.breadcrumbs.get(0); + int count = result.breadcrumbs.size(); + for (int i = 1; i < count; i++) { + breadcrumb = getContext().getString(R.string.search_breadcrumb_connector, + breadcrumb, result.breadcrumbs.get(i)); + } + + return breadcrumb; + } + + /** + * Gets called when a search result is clicked. + */ + protected void onSearchResultClicked(SearchResult result) { + mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); + mSavedQueryController.saveQuery(mQuery); + + Intent intent = result.payload.getIntent(); + if (result instanceof AppSearchResult) { + getActivity().startActivity(intent); + } else { + PackageManager pm = getActivity().getPackageManager(); + List<ResolveInfo> info = pm.queryIntentActivities(intent, /* flags= */ 0); + if (info != null && !info.isEmpty()) { + startActivityForResult(intent, REQUEST_CODE_NO_OP); + } else { + Log.e(TAG, "Cannot launch search result, title: " + + result.title + ", " + intent); + } + } + } + + @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/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/car/CarSavedQueryController.java b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryController.java new file mode 100644 index 0000000..4c9a458 --- /dev/null +++ b/src/com/android/settings/intelligence/search/savedqueries/car/CarSavedQueryController.java @@ -0,0 +1,161 @@ +/* + * 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.annotation.NonNull; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + +import com.android.car.ui.imewidescreen.CarUiImeSearchListItem; +import com.android.car.ui.recyclerview.CarUiContentListItem; +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.car.CarSearchFragment; +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.ArrayList; +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; + private ToolbarController mToolbar; + private CarSearchFragment mFragment; + + public CarSavedQueryController(Context context, LoaderManager loaderManager, + CarSearchResultsAdapter resultsAdapter, @NonNull ToolbarController toolbar, + CarSearchFragment fragment) { + mContext = context; + mLoaderManager = loaderManager; + mResultAdapter = resultsAdapter; + mSearchFeatureProvider = FeatureFactory.get(context) + .searchFeatureProvider(); + mToolbar = toolbar; + mFragment = fragment; + } + + @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"); + } + List<SearchResult> results = (List<SearchResult>) data; + if (mToolbar.canShowSearchResultItems()) { + List<CarUiImeSearchListItem> searchItems = new ArrayList<>(); + for (SearchResult result : results) { + CarUiImeSearchListItem item = new CarUiImeSearchListItem( + CarUiContentListItem.Action.ICON); + item.setTitle(result.title); + item.setIconResId(R.drawable.ic_restore); + item.setOnItemClickedListener( + v -> mFragment.onSavedQueryClicked(result.title)); + + searchItems.add(item); + } + mToolbar.setSearchResultItems(searchItems); + } + + mResultAdapter.displaySavedQuery(results); + 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..12051a8 --- /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(result.title); + }); + } +} |