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