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