1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.intelligence.search.car; 18 19 import static com.android.car.ui.core.CarUi.requireInsets; 20 import static com.android.car.ui.core.CarUi.requireToolbar; 21 import static com.android.car.ui.utils.CarUiUtils.drawableToBitmap; 22 23 import android.app.Activity; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.graphics.Bitmap; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.os.Bundle; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.inputmethod.InputMethodManager; 36 37 import androidx.annotation.NonNull; 38 import androidx.loader.app.LoaderManager; 39 import androidx.loader.content.Loader; 40 41 import com.android.car.ui.imewidescreen.CarUiImeSearchListItem; 42 import com.android.car.ui.preference.PreferenceFragment; 43 import com.android.car.ui.recyclerview.CarUiContentListItem; 44 import com.android.car.ui.recyclerview.CarUiRecyclerView; 45 import com.android.car.ui.toolbar.MenuItem; 46 import com.android.car.ui.toolbar.NavButtonMode; 47 import com.android.car.ui.toolbar.SearchConfig; 48 import com.android.car.ui.toolbar.SearchMode; 49 import com.android.car.ui.toolbar.ToolbarController; 50 import com.android.settings.intelligence.R; 51 import com.android.settings.intelligence.overlay.FeatureFactory; 52 import com.android.settings.intelligence.search.AppSearchResult; 53 import com.android.settings.intelligence.search.SearchCommon; 54 import com.android.settings.intelligence.search.SearchFeatureProvider; 55 import com.android.settings.intelligence.search.SearchResult; 56 import com.android.settings.intelligence.search.indexing.IndexingCallback; 57 import com.android.settings.intelligence.search.savedqueries.car.CarSavedQueryController; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 62 /** 63 * Search fragment for car settings. 64 */ 65 public class CarSearchFragment extends PreferenceFragment implements 66 LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback { 67 private static final String TAG = "CarSearchFragment"; 68 private static final int REQUEST_CODE_NO_OP = 0; 69 70 private SearchFeatureProvider mSearchFeatureProvider; 71 72 private ToolbarController mToolbar; 73 private CarUiRecyclerView mRecyclerView; 74 75 private String mQuery; 76 private boolean mShowingSavedQuery; 77 78 private CarSearchResultsAdapter mSearchAdapter; 79 private CarSavedQueryController mSavedQueryController; 80 81 private final CarUiRecyclerView.OnScrollListener mScrollListener = 82 new CarUiRecyclerView.OnScrollListener() { 83 @Override 84 public void onScrolled(@NonNull CarUiRecyclerView recyclerView, int dx, int dy) { 85 if (dy != 0) { 86 hideKeyboard(); 87 } 88 } 89 90 @Override 91 public void onScrollStateChanged(@NonNull CarUiRecyclerView recyclerView, 92 int newState) {} 93 94 }; 95 96 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)97 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 98 setPreferencesFromResource(R.xml.car_search_fragment, rootKey); 99 } 100 getToolbar()101 protected ToolbarController getToolbar() { 102 return requireToolbar(requireActivity()); 103 } 104 getToolbarMenuItems()105 protected List<MenuItem> getToolbarMenuItems() { 106 return null; 107 } 108 109 @Override onAttach(Context context)110 public void onAttach(Context context) { 111 super.onAttach(context); 112 mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider(); 113 } 114 115 @Override onCreate(Bundle savedInstanceState)116 public void onCreate(Bundle savedInstanceState) { 117 super.onCreate(savedInstanceState); 118 119 if (savedInstanceState != null) { 120 mQuery = savedInstanceState.getString(SearchCommon.STATE_QUERY); 121 mShowingSavedQuery = savedInstanceState.getBoolean( 122 SearchCommon.STATE_SHOWING_SAVED_QUERY); 123 } else { 124 mShowingSavedQuery = true; 125 } 126 127 LoaderManager loaderManager = getLoaderManager(); 128 mSearchAdapter = new CarSearchResultsAdapter(/* fragment= */ this); 129 mToolbar = getToolbar(); 130 mSavedQueryController = new CarSavedQueryController( 131 getContext(), loaderManager, mSearchAdapter, mToolbar, this); 132 mSearchFeatureProvider.updateIndexAsync(getContext(), /* indexingCallback= */ this); 133 } 134 135 @Override onActivityCreated(Bundle savedInstanceState)136 public void onActivityCreated(Bundle savedInstanceState) { 137 super.onActivityCreated(savedInstanceState); 138 if (mToolbar != null) { 139 List<MenuItem> items = getToolbarMenuItems(); 140 mToolbar.setTitle(getPreferenceScreen().getTitle()); 141 mToolbar.setMenuItems(items); 142 mToolbar.setNavButtonMode(NavButtonMode.BACK); 143 mToolbar.setSearchMode(SearchMode.SEARCH); 144 mToolbar.setSearchHint(R.string.abc_search_hint); 145 mToolbar.registerSearchListener(this::onQueryTextChange); 146 mToolbar.registerSearchCompletedListener(this::onSearchComplete); 147 mToolbar.setShowMenuItemsWhileSearching(true); 148 mToolbar.setSearchQuery(mQuery); 149 } 150 mRecyclerView = getCarUiRecyclerView(); 151 if (mRecyclerView != null) { 152 mRecyclerView.setAdapter(mSearchAdapter); 153 mRecyclerView.addOnScrollListener(mScrollListener); 154 } 155 } 156 157 @Override onStart()158 public void onStart() { 159 super.onStart(); 160 onCarUiInsetsChanged(requireInsets(requireActivity())); 161 } 162 163 @Override onSaveInstanceState(@onNull Bundle outState)164 public void onSaveInstanceState(@NonNull Bundle outState) { 165 super.onSaveInstanceState(outState); 166 outState.putString(SearchCommon.STATE_QUERY, mQuery); 167 outState.putBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery); 168 } 169 onQueryTextChange(String query)170 private void onQueryTextChange(String query) { 171 if (TextUtils.equals(query, mQuery)) { 172 return; 173 } 174 boolean isEmptyQuery = TextUtils.isEmpty(query); 175 176 mQuery = query; 177 178 // If indexing is not finished, register the query text, but don't search. 179 if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) { 180 mToolbar.getProgressBar().setVisible(!isEmptyQuery); 181 return; 182 } 183 184 if (isEmptyQuery) { 185 LoaderManager loaderManager = getLoaderManager(); 186 loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT); 187 mShowingSavedQuery = true; 188 mSavedQueryController.loadSavedQueries(); 189 } else { 190 restartLoaders(); 191 } 192 } 193 onSearchComplete()194 private void onSearchComplete() { 195 if (!TextUtils.isEmpty(mQuery)) { 196 mSavedQueryController.saveQuery(mQuery); 197 } 198 } 199 200 /** 201 * Gets called when a saved query is clicked. 202 */ onSavedQueryClicked(CharSequence query)203 public void onSavedQueryClicked(CharSequence query) { 204 String queryString = query.toString(); 205 mToolbar.setSearchQuery(queryString); 206 onQueryTextChange(queryString); 207 hideKeyboard(); 208 } 209 210 @Override onCreateLoader(int id, Bundle args)211 public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) { 212 Activity activity = getActivity(); 213 214 if (id == SearchCommon.SearchLoaderId.SEARCH_RESULT) { 215 return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery); 216 } 217 return null; 218 } 219 220 @Override onLoadFinished(Loader<List<? extends SearchResult>> loader, List<? extends SearchResult> data)221 public void onLoadFinished(Loader<List<? extends SearchResult>> loader, 222 List<? extends SearchResult> data) { 223 224 if (mToolbar.getSearchCapabilities().canShowSearchResultItems()) { 225 List<CarUiImeSearchListItem> searchItems = new ArrayList<>(); 226 for (SearchResult result : data) { 227 CarUiImeSearchListItem item = new CarUiImeSearchListItem( 228 CarUiContentListItem.Action.ICON); 229 item.setTitle(result.title); 230 if (result.breadcrumbs != null && !result.breadcrumbs.isEmpty()) { 231 item.setBody(getBreadcrumb(result)); 232 } 233 234 if (result instanceof AppSearchResult) { 235 AppSearchResult appResult = (AppSearchResult) result; 236 PackageManager pm = getActivity().getPackageManager(); 237 Drawable drawable = appResult.info.loadIcon(pm); 238 Bitmap bm = drawableToBitmap(drawable); 239 BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); 240 item.setIcon(bitmapDrawable); 241 } else if (result.icon != null) { 242 Bitmap bm = drawableToBitmap(result.icon); 243 BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); 244 item.setIcon(bitmapDrawable); 245 } 246 item.setOnItemClickedListener(v -> onSearchResultClicked(result)); 247 248 searchItems.add(item); 249 } 250 mToolbar.setSearchConfig(SearchConfig.builder() 251 .setSearchResultItems(searchItems) 252 .build()); 253 } 254 255 mSearchAdapter.postSearchResults(data); 256 mRecyclerView.scrollToPosition(0); 257 } 258 getBreadcrumb(SearchResult result)259 private String getBreadcrumb(SearchResult result) { 260 String breadcrumb = result.breadcrumbs.get(0); 261 int count = result.breadcrumbs.size(); 262 for (int i = 1; i < count; i++) { 263 breadcrumb = getContext().getString(R.string.search_breadcrumb_connector, 264 breadcrumb, result.breadcrumbs.get(i)); 265 } 266 267 return breadcrumb; 268 } 269 270 /** 271 * Gets called when a search result is clicked. 272 */ onSearchResultClicked(SearchResult result)273 protected void onSearchResultClicked(SearchResult result) { 274 mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); 275 mSavedQueryController.saveQuery(mQuery); 276 277 // Hide keyboard to apply the proper insets before the activity launches. 278 // TODO (b/187074444): remove if WindowManager updates ordering of insets such that they are 279 // applied before new activities are launched. 280 hideKeyboard(); 281 282 Intent intent = result.payload.getIntent(); 283 if (result instanceof AppSearchResult) { 284 getActivity().startActivity(intent); 285 } else { 286 PackageManager pm = getActivity().getPackageManager(); 287 List<ResolveInfo> info = pm.queryIntentActivities(intent, /* flags= */ 0); 288 if (info != null && !info.isEmpty()) { 289 startActivityForResult(intent, REQUEST_CODE_NO_OP); 290 } else { 291 Log.e(TAG, "Cannot launch search result, title: " 292 + result.title + ", " + intent); 293 } 294 } 295 } 296 297 @Override onLoaderReset(Loader<List<? extends SearchResult>> loader)298 public void onLoaderReset(Loader<List<? extends SearchResult>> loader) { 299 } 300 301 /** 302 * Gets called when Indexing is completed. 303 */ 304 @Override onIndexingFinished()305 public void onIndexingFinished() { 306 if (getActivity() == null) { 307 return; 308 } 309 mToolbar.getProgressBar().setVisible(false); 310 if (mShowingSavedQuery) { 311 mSavedQueryController.loadSavedQueries(); 312 } else { 313 LoaderManager loaderManager = getLoaderManager(); 314 loaderManager.initLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, 315 /* args= */ null, /* callback= */ this); 316 } 317 requery(); 318 } 319 requery()320 private void requery() { 321 if (TextUtils.isEmpty(mQuery)) { 322 return; 323 } 324 String query = mQuery; 325 mQuery = ""; 326 onQueryTextChange(query); 327 } 328 restartLoaders()329 private void restartLoaders() { 330 mShowingSavedQuery = false; 331 LoaderManager loaderManager = getLoaderManager(); 332 loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, 333 /* args= */ null, /* callback= */ this); 334 } 335 hideKeyboard()336 private void hideKeyboard() { 337 Activity activity = getActivity(); 338 if (activity != null) { 339 View view = activity.getCurrentFocus(); 340 InputMethodManager imm = (InputMethodManager) 341 activity.getSystemService(Context.INPUT_METHOD_SERVICE); 342 if (imm.isActive(view)) { 343 imm.hideSoftInputFromWindow(view.getWindowToken(), /* flags= */ 0); 344 } 345 } 346 347 if (mRecyclerView != null && !mRecyclerView.getView().hasFocus()) { 348 mRecyclerView.getView().requestFocus(); 349 } 350 } 351 } 352