SearchFragment.java revision 4c0f3062b5edd9750351068f46e5270bb220091d
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import android.app.Fragment;
17import android.graphics.drawable.Drawable;
18import android.os.Bundle;
19import android.os.Handler;
20import android.speech.SpeechRecognizer;
21import android.support.v17.leanback.widget.ObjectAdapter;
22import android.support.v17.leanback.widget.OnItemClickedListener;
23import android.support.v17.leanback.widget.OnItemSelectedListener;
24import android.support.v17.leanback.widget.Row;
25import android.support.v17.leanback.widget.SearchBar;
26import android.support.v17.leanback.widget.VerticalGridView;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.FrameLayout;
32import android.support.v17.leanback.R;
33
34import java.util.List;
35
36/**
37 * A fragment to handle searches. An application will supply an implementation
38 * of the {@link SearchResultProvider} interface to handle the search and return
39 * an {@link ObjectAdapter} containing the results. The results are rendered
40 * into a {@link RowsFragment}, in the same way that they are in a {@link
41 * BrowseFragment}.
42 *
43 * <p>Note: Your application will need to request android.permission.RECORD_AUDIO.
44 */
45public class SearchFragment extends Fragment {
46    private static final String TAG = SearchFragment.class.getSimpleName();
47    private static final boolean DEBUG = false;
48
49    private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
50    private static final String ARG_QUERY =  ARG_PREFIX + ".query";
51    private static final String ARG_TITLE = ARG_PREFIX  + ".title";
52
53    /**
54     * Search API to be provided by the application.
55     */
56    public static interface SearchResultProvider {
57        /**
58         * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve
59         * an ObjectAdapter that will contain the results to future updates of the search query.</p>
60         *
61         * <p>As results are retrieved, the application should use the data set notification methods
62         * on the ObjectAdapter to instruct the SearchFragment to update the results.</p>
63         *
64         * @return ObjectAdapter The result object adapter.
65         */
66        public ObjectAdapter getResultsAdapter();
67
68        /**
69         * <p>Method invoked when the search query is updated.</p>
70         *
71         * <p>This is called as soon as the query changes; it is up to the application to add a
72         * delay before actually executing the queries if needed.
73         *
74         * <p>This method might not always be called before onQueryTextSubmit gets called, in
75         * particular for voice input.
76         *
77         * @param newQuery The current search query.
78         * @return whether the results changed as a result of the new query.
79         */
80        public boolean onQueryTextChange(String newQuery);
81
82        /**
83         * Method invoked when the search query is submitted, either by dismissing the keyboard,
84         * pressing search or next on the keyboard or when voice has detected the end of the query.
85         *
86         * @param query The query entered.
87         * @return whether the results changed as a result of the query.
88         */
89        public boolean onQueryTextSubmit(String query);
90    }
91
92    private RowsFragment mRowsFragment;
93    private final Handler mHandler = new Handler();
94
95    private SearchBar mSearchBar;
96    private SearchResultProvider mProvider;
97    private String mPendingQuery = null;
98
99    private OnItemSelectedListener mOnItemSelectedListener;
100    private OnItemClickedListener mOnItemClickedListener;
101    private ObjectAdapter mResultAdapter;
102
103    private String mTitle;
104    private Drawable mBadgeDrawable;
105
106    private SpeechRecognizer mSpeechRecognizer;
107
108    /**
109     * @param args Bundle to use for the arguments, if null a new Bundle will be created.
110     */
111    public static Bundle createArgs(Bundle args, String query) {
112        return createArgs(args, query, null);
113    }
114
115    public static Bundle createArgs(Bundle args, String query, String title)  {
116        if (args == null) {
117            args = new Bundle();
118        }
119        args.putString(ARG_QUERY, query);
120        args.putString(ARG_TITLE, title);
121        return args;
122    }
123
124    /**
125     * Create a search fragment with a given search query.
126     *
127     * <p>You should only use this if you need to start the search fragment with a
128     * pre-filled query.
129     *
130     * @param query The search query to begin with.
131     * @return A new SearchFragment.
132     */
133    public static SearchFragment newInstance(String query) {
134        SearchFragment fragment = new SearchFragment();
135        Bundle args = createArgs(null, query);
136        fragment.setArguments(args);
137        return fragment;
138    }
139
140    @Override
141    public void onCreate(Bundle savedInstanceState) {
142        super.onCreate(savedInstanceState);
143    }
144
145    @Override
146    public View onCreateView(LayoutInflater inflater, ViewGroup container,
147                             Bundle savedInstanceState) {
148        View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
149
150        FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame);
151        mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar);
152        mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
153            @Override
154            public void onSearchQueryChange(String query) {
155                if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s", query));
156                if (null != mProvider) {
157                    retrieveResults(query);
158                } else {
159                    mPendingQuery = query;
160                }
161            }
162
163            @Override
164            public void onSearchQuerySubmit(String query) {
165                if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query));
166                mRowsFragment.setSelectedPosition(0);
167                mRowsFragment.getVerticalGridView().requestFocus();
168                if (null != mProvider) {
169                    mProvider.onQueryTextSubmit(query);
170                }
171            }
172
173            @Override
174            public void onKeyboardDismiss(String query) {
175                if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query));
176                mRowsFragment.setSelectedPosition(0);
177                mRowsFragment.getVerticalGridView().requestFocus();
178            }
179        });
180
181        readArguments(getArguments());
182        if (null != mBadgeDrawable) {
183            setBadgeDrawable(mBadgeDrawable);
184        }
185        if (null != mTitle) {
186            setTitle(mTitle);
187        }
188
189        // Inject the RowsFragment in the results container
190        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
191            mRowsFragment = new RowsFragment();
192            getChildFragmentManager().beginTransaction()
193                    .replace(R.id.lb_results_frame, mRowsFragment).commit();
194        } else {
195            mRowsFragment = (RowsFragment) getChildFragmentManager()
196                    .findFragmentById(R.id.browse_container_dock);
197        }
198        mRowsFragment.setOnItemSelectedListener(new OnItemSelectedListener() {
199            @Override
200            public void onItemSelected(Object item, Row row) {
201                int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
202                if (DEBUG) Log.v(TAG, String.format("onItemSelected %d", position));
203                mSearchBar.setVisibility(0 >= position ? View.VISIBLE : View.GONE);
204                if (null != mOnItemSelectedListener) {
205                    mOnItemSelectedListener.onItemSelected(item, row);
206                }
207            }
208        });
209        mRowsFragment.setOnItemClickedListener(new OnItemClickedListener() {
210            @Override
211            public void onItemClicked(Object item, Row row) {
212                int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
213                if (DEBUG) Log.v(TAG, String.format("onItemClicked %d", position));
214                if (null != mOnItemClickedListener) {
215                    mOnItemClickedListener.onItemClicked(item, row);
216                }
217            }
218        });
219        mRowsFragment.setExpand(true);
220        if (null != mProvider) {
221            onSetSearchResultProvider();
222        }
223        return root;
224    }
225
226    @Override
227    public void onStart() {
228        super.onStart();
229
230        VerticalGridView list = mRowsFragment.getVerticalGridView();
231        int mContainerListAlignTop =
232                getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top);
233        list.setItemAlignmentOffset(0);
234        list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
235        list.setWindowAlignmentOffset(mContainerListAlignTop);
236        list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
237        list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
238    }
239
240    @Override
241    public void onResume() {
242        super.onResume();
243        if (null == mSpeechRecognizer) {
244            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
245            mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
246        }
247    }
248
249    @Override
250    public void onPause() {
251        if (null != mSpeechRecognizer) {
252            mSearchBar.setSpeechRecognizer(null);
253            mSpeechRecognizer.destroy();
254            mSpeechRecognizer = null;
255        }
256        super.onPause();
257    }
258
259    /**
260     * Set the search provider that is responsible for returning results for the
261     * search query.
262     */
263    public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
264        mProvider = searchResultProvider;
265        onSetSearchResultProvider();
266    }
267
268    /**
269     * Sets an item selection listener for the results.
270     *
271     * @param listener The item selection listener to be invoked when an item in
272     *        the search results is selected.
273     */
274    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
275        mOnItemSelectedListener = listener;
276    }
277
278    /**
279     * Sets an item clicked listener for the results.
280     *
281     * @param listener The item clicked listener to be invoked when an item in
282     *        the search results is clicked.
283     */
284    public void setOnItemClickedListener(OnItemClickedListener listener) {
285        mOnItemClickedListener = listener;
286    }
287
288    /**
289     * Sets the title string to be be shown in an empty search bar. The title
290     * may be placed in a call-to-action, such as "Search <i>title</i>" or
291     * "Speak to search <i>title</i>".
292     */
293    public void setTitle(String title) {
294        mTitle = title;
295        if (null != mSearchBar) {
296            mSearchBar.setTitle(title);
297        }
298    }
299
300    /**
301     * Returns the title set in the search bar.
302     */
303    public String getTitle() {
304        if (null != mSearchBar) {
305            return mSearchBar.getTitle();
306        }
307        return null;
308    }
309
310    /**
311     * Sets the badge drawable that will be shown inside the search bar next to
312     * the title.
313     */
314    public void setBadgeDrawable(Drawable drawable) {
315        mBadgeDrawable = drawable;
316        if (null != mSearchBar) {
317            mSearchBar.setBadgeDrawable(drawable);
318        }
319    }
320
321    /**
322     * Returns the badge drawable in the search bar.
323     */
324    public Drawable getBadgeDrawable() {
325        if (null != mSearchBar) {
326            return mSearchBar.getBadgeDrawable();
327        }
328        return null;
329    }
330
331    /**
332     * Display the completions shown by the IME. An application may provide
333     * a list of query completions that the system will show in the IME.
334     *
335     * @param completions A list of completions to show in the IME. Setting to
336     *        null or empty will clear the list.
337     */
338    public void displayCompletions(List<String> completions) {
339        mSearchBar.displayCompletions(completions);
340    }
341
342    private void retrieveResults(String searchQuery) {
343        if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery));
344        mProvider.onQueryTextChange(searchQuery);
345    }
346
347    private void onSetSearchResultProvider() {
348        mHandler.post(new Runnable() {
349            @Override
350            public void run() {
351                // Retrieve the result adapter
352                mResultAdapter = mProvider.getResultsAdapter();
353                if (null != mRowsFragment) {
354                    mRowsFragment.setAdapter(mResultAdapter);
355                    executePendingQuery();
356                }
357            }
358        });
359    }
360
361    private void executePendingQuery() {
362        if (null != mPendingQuery && null != mResultAdapter) {
363            String query = mPendingQuery;
364            mPendingQuery = null;
365            retrieveResults(query);
366        }
367    }
368
369    private void readArguments(Bundle args) {
370        if (null == args) {
371            return;
372        }
373        if (args.containsKey(ARG_QUERY)) {
374            setSearchQuery(args.getString(ARG_QUERY));
375        }
376
377        if (args.containsKey(ARG_TITLE)) {
378            setTitle(args.getString(ARG_TITLE));
379        }
380    }
381
382    private void setSearchQuery(String query) {
383        mSearchBar.setSearchQuery(query);
384    }
385}
386