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 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)96 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 97 setPreferencesFromResource(R.xml.car_search_fragment, rootKey); 98 } 99 getToolbar()100 protected ToolbarController getToolbar() { 101 return requireToolbar(requireActivity()); 102 } 103 getToolbarMenuItems()104 protected List<MenuItem> getToolbarMenuItems() { 105 return null; 106 } 107 108 @Override onAttach(Context context)109 public void onAttach(Context context) { 110 super.onAttach(context); 111 mSearchFeatureProvider = FeatureFactory.get(context).searchFeatureProvider(); 112 } 113 114 @Override onCreate(Bundle savedInstanceState)115 public void onCreate(Bundle savedInstanceState) { 116 super.onCreate(savedInstanceState); 117 118 if (savedInstanceState != null) { 119 mQuery = savedInstanceState.getString(SearchCommon.STATE_QUERY); 120 mShowingSavedQuery = savedInstanceState.getBoolean( 121 SearchCommon.STATE_SHOWING_SAVED_QUERY); 122 } else { 123 mShowingSavedQuery = true; 124 } 125 126 LoaderManager loaderManager = getLoaderManager(); 127 mSearchAdapter = new CarSearchResultsAdapter(/* fragment= */ this); 128 mToolbar = getToolbar(); 129 mSavedQueryController = new CarSavedQueryController( 130 getContext(), loaderManager, mSearchAdapter, mToolbar, this); 131 mSearchFeatureProvider.updateIndexAsync(getContext(), /* indexingCallback= */ this); 132 } 133 134 @Override onActivityCreated(Bundle savedInstanceState)135 public void onActivityCreated(Bundle savedInstanceState) { 136 super.onActivityCreated(savedInstanceState); 137 if (mToolbar != null) { 138 List<MenuItem> items = getToolbarMenuItems(); 139 mToolbar.setTitle(getPreferenceScreen().getTitle()); 140 mToolbar.setMenuItems(items); 141 mToolbar.setNavButtonMode(NavButtonMode.BACK); 142 mToolbar.setSearchMode(SearchMode.SEARCH); 143 mToolbar.setSearchHint(R.string.abc_search_hint); 144 mToolbar.registerSearchListener(this::onQueryTextChange); 145 mToolbar.registerSearchCompletedListener(this::onSearchComplete); 146 mToolbar.setShowMenuItemsWhileSearching(true); 147 mToolbar.setSearchQuery(mQuery); 148 } 149 mRecyclerView = getCarUiRecyclerView(); 150 if (mRecyclerView != null) { 151 mRecyclerView.setAdapter(mSearchAdapter); 152 mRecyclerView.addOnScrollListener(mScrollListener); 153 } 154 } 155 156 @Override onStart()157 public void onStart() { 158 super.onStart(); 159 onCarUiInsetsChanged(requireInsets(requireActivity())); 160 } 161 162 @Override onSaveInstanceState(@onNull Bundle outState)163 public void onSaveInstanceState(@NonNull Bundle outState) { 164 super.onSaveInstanceState(outState); 165 outState.putString(SearchCommon.STATE_QUERY, mQuery); 166 outState.putBoolean(SearchCommon.STATE_SHOWING_SAVED_QUERY, mShowingSavedQuery); 167 } 168 onQueryTextChange(String query)169 private void onQueryTextChange(String query) { 170 if (TextUtils.equals(query, mQuery)) { 171 return; 172 } 173 boolean isEmptyQuery = TextUtils.isEmpty(query); 174 175 mQuery = query; 176 177 // If indexing is not finished, register the query text, but don't search. 178 if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) { 179 mToolbar.getProgressBar().setVisible(!isEmptyQuery); 180 return; 181 } 182 183 if (isEmptyQuery) { 184 LoaderManager loaderManager = getLoaderManager(); 185 loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT); 186 mShowingSavedQuery = true; 187 mSavedQueryController.loadSavedQueries(); 188 } else { 189 restartLoaders(); 190 } 191 } 192 onSearchComplete()193 private void onSearchComplete() { 194 if (!TextUtils.isEmpty(mQuery)) { 195 mSavedQueryController.saveQuery(mQuery); 196 } 197 } 198 199 /** 200 * Gets called when a saved query is clicked. 201 */ onSavedQueryClicked(CharSequence query)202 public void onSavedQueryClicked(CharSequence query) { 203 String queryString = query.toString(); 204 mToolbar.setSearchQuery(queryString); 205 onQueryTextChange(queryString); 206 hideKeyboard(); 207 } 208 209 @Override onCreateLoader(int id, Bundle args)210 public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) { 211 Activity activity = getActivity(); 212 213 if (id == SearchCommon.SearchLoaderId.SEARCH_RESULT) { 214 return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery); 215 } 216 return null; 217 } 218 219 @Override onLoadFinished(Loader<List<? extends SearchResult>> loader, List<? extends SearchResult> data)220 public void onLoadFinished(Loader<List<? extends SearchResult>> loader, 221 List<? extends SearchResult> data) { 222 223 if (mToolbar.getSearchCapabilities().canShowSearchResultItems()) { 224 List<CarUiImeSearchListItem> searchItems = new ArrayList<>(); 225 for (SearchResult result : data) { 226 CarUiImeSearchListItem item = new CarUiImeSearchListItem( 227 CarUiContentListItem.Action.ICON); 228 item.setTitle(result.title); 229 if (result.breadcrumbs != null && !result.breadcrumbs.isEmpty()) { 230 item.setBody(getBreadcrumb(result)); 231 } 232 233 if (result instanceof AppSearchResult) { 234 AppSearchResult appResult = (AppSearchResult) result; 235 PackageManager pm = getActivity().getPackageManager(); 236 Drawable drawable = appResult.info.loadIcon(pm); 237 Bitmap bm = drawableToBitmap(drawable); 238 BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); 239 item.setIcon(bitmapDrawable); 240 } else if (result.icon != null) { 241 Bitmap bm = drawableToBitmap(result.icon); 242 BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), bm); 243 item.setIcon(bitmapDrawable); 244 } 245 item.setOnItemClickedListener(v -> onSearchResultClicked(result)); 246 247 searchItems.add(item); 248 } 249 mToolbar.setSearchConfig(SearchConfig.builder() 250 .setSearchResultItems(searchItems) 251 .build()); 252 } 253 254 mSearchAdapter.postSearchResults(data); 255 mRecyclerView.scrollToPosition(0); 256 } 257 getBreadcrumb(SearchResult result)258 private String getBreadcrumb(SearchResult result) { 259 String breadcrumb = result.breadcrumbs.get(0); 260 int count = result.breadcrumbs.size(); 261 for (int i = 1; i < count; i++) { 262 breadcrumb = getContext().getString(R.string.search_breadcrumb_connector, 263 breadcrumb, result.breadcrumbs.get(i)); 264 } 265 266 return breadcrumb; 267 } 268 269 /** 270 * Gets called when a search result is clicked. 271 */ onSearchResultClicked(SearchResult result)272 protected void onSearchResultClicked(SearchResult result) { 273 mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); 274 mSavedQueryController.saveQuery(mQuery); 275 276 // Hide keyboard to apply the proper insets before the activity launches. 277 // TODO (b/187074444): remove if WindowManager updates ordering of insets such that they are 278 // applied before new activities are launched. 279 hideKeyboard(); 280 281 Intent intent = result.payload.getIntent(); 282 if (result instanceof AppSearchResult) { 283 getActivity().startActivity(intent); 284 } else { 285 PackageManager pm = getActivity().getPackageManager(); 286 List<ResolveInfo> info = pm.queryIntentActivities(intent, /* flags= */ 0); 287 if (info != null && !info.isEmpty()) { 288 startActivityForResult(intent, REQUEST_CODE_NO_OP); 289 } else { 290 Log.e(TAG, "Cannot launch search result, title: " 291 + result.title + ", " + intent); 292 } 293 } 294 } 295 296 @Override onLoaderReset(Loader<List<? extends SearchResult>> loader)297 public void onLoaderReset(Loader<List<? extends SearchResult>> loader) { 298 } 299 300 /** 301 * Gets called when Indexing is completed. 302 */ 303 @Override onIndexingFinished()304 public void onIndexingFinished() { 305 if (getActivity() == null) { 306 return; 307 } 308 mToolbar.getProgressBar().setVisible(false); 309 if (mShowingSavedQuery) { 310 mSavedQueryController.loadSavedQueries(); 311 } else { 312 LoaderManager loaderManager = getLoaderManager(); 313 loaderManager.initLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, 314 /* args= */ null, /* callback= */ this); 315 } 316 requery(); 317 } 318 requery()319 private void requery() { 320 if (TextUtils.isEmpty(mQuery)) { 321 return; 322 } 323 String query = mQuery; 324 mQuery = ""; 325 onQueryTextChange(query); 326 } 327 restartLoaders()328 private void restartLoaders() { 329 mShowingSavedQuery = false; 330 LoaderManager loaderManager = getLoaderManager(); 331 loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT, 332 /* args= */ null, /* callback= */ this); 333 } 334 hideKeyboard()335 private void hideKeyboard() { 336 Activity activity = getActivity(); 337 if (activity != null) { 338 View view = activity.getCurrentFocus(); 339 InputMethodManager imm = (InputMethodManager) 340 activity.getSystemService(Context.INPUT_METHOD_SERVICE); 341 if (imm.isActive(view)) { 342 imm.hideSoftInputFromWindow(view.getWindowToken(), /* flags= */ 0); 343 } 344 } 345 346 if (mRecyclerView != null && !mRecyclerView.getView().hasFocus()) { 347 mRecyclerView.getView().requestFocus(); 348 } 349 } 350 } 351