1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.search.autocomplete; 6 7 import java.util.ArrayList; 8 import java.util.List; 9 10 import android.support.v4.content.ContextCompat; 11 import org.mozilla.gecko.R; 12 import org.mozilla.gecko.search.SearchEngine; 13 import org.mozilla.gecko.SuggestClient; 14 import org.mozilla.gecko.Telemetry; 15 import org.mozilla.gecko.TelemetryContract; 16 import org.mozilla.search.AcceptsSearchQuery; 17 import org.mozilla.search.AcceptsSearchQuery.SuggestionAnimation; 18 19 import android.content.Context; 20 import android.graphics.Rect; 21 import android.os.Bundle; 22 import android.support.v4.app.Fragment; 23 import android.support.v4.app.LoaderManager; 24 import android.support.v4.content.AsyncTaskLoader; 25 import android.support.v4.content.Loader; 26 import android.text.SpannableString; 27 import android.text.style.ForegroundColorSpan; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.AdapterView; 33 import android.widget.ListView; 34 35 /** 36 * A fragment to show search suggestions. 37 */ 38 public class SuggestionsFragment extends Fragment { 39 40 private static final String LOG_TAG = "SuggestionsFragment"; 41 42 private static final int LOADER_ID_SUGGESTION = 0; 43 private static final String KEY_SEARCH_TERM = "search_term"; 44 45 // Timeout for the suggestion client to respond 46 private static final int SUGGESTION_TIMEOUT = 3000; 47 48 // Number of search suggestions to show. 49 private static final int SUGGESTION_MAX = 5; 50 51 public static final String GECKO_SEARCH_TERMS_URL_PARAM = "__searchTerms__"; 52 53 private AcceptsSearchQuery searchListener; 54 55 // Suggest client gets setup outside of the normal fragment lifecycle, therefore 56 // clients should ensure that this isn't null before using it. 57 private SuggestClient suggestClient; 58 private SuggestionLoaderCallbacks suggestionLoaderCallbacks; 59 60 private AutoCompleteAdapter autoCompleteAdapter; 61 62 // Holds the list of search suggestions. 63 private ListView suggestionsList; 64 SuggestionsFragment()65 public SuggestionsFragment() { 66 // Required empty public constructor 67 } 68 69 @Override onAttach(Context context)70 public void onAttach(Context context) { 71 super.onAttach(context); 72 73 if (context instanceof AcceptsSearchQuery) { 74 searchListener = (AcceptsSearchQuery) context; 75 } else { 76 throw new ClassCastException(context.toString() + " must implement AcceptsSearchQuery."); 77 } 78 79 suggestionLoaderCallbacks = new SuggestionLoaderCallbacks(); 80 autoCompleteAdapter = new AutoCompleteAdapter(context); 81 } 82 83 @Override onDetach()84 public void onDetach() { 85 super.onDetach(); 86 87 searchListener = null; 88 suggestionLoaderCallbacks = null; 89 autoCompleteAdapter = null; 90 suggestClient = null; 91 } 92 93 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)94 public View onCreateView(LayoutInflater inflater, ViewGroup container, 95 Bundle savedInstanceState) { 96 suggestionsList = (ListView) inflater.inflate(R.layout.search_sugestions, container, false); 97 suggestionsList.setAdapter(autoCompleteAdapter); 98 99 // Attach listener for tapping on a suggestion. 100 suggestionsList.setOnItemClickListener(new AdapterView.OnItemClickListener() { 101 @Override 102 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 103 final Suggestion suggestion = (Suggestion) suggestionsList.getItemAtPosition(position); 104 105 final Rect startBounds = new Rect(); 106 view.getGlobalVisibleRect(startBounds); 107 108 // The user tapped on a suggestion from the search engine. 109 Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, TelemetryContract.Method.SUGGESTION, position); 110 111 searchListener.onSearch(suggestion.value, new SuggestionAnimation() { 112 @Override 113 public Rect getStartBounds() { 114 return startBounds; 115 } 116 }); 117 } 118 }); 119 120 return suggestionsList; 121 } 122 123 @Override onDestroyView()124 public void onDestroyView() { 125 super.onDestroyView(); 126 127 if (null != suggestionsList) { 128 suggestionsList.setOnItemClickListener(null); 129 suggestionsList.setAdapter(null); 130 suggestionsList = null; 131 } 132 } 133 setEngine(SearchEngine engine)134 public void setEngine(SearchEngine engine) { 135 suggestClient = new SuggestClient(getActivity(), engine.getSuggestionTemplate(GECKO_SEARCH_TERMS_URL_PARAM), 136 SUGGESTION_TIMEOUT, SUGGESTION_MAX, true); 137 } 138 loadSuggestions(String query)139 public void loadSuggestions(String query) { 140 final Bundle args = new Bundle(); 141 args.putString(KEY_SEARCH_TERM, query); 142 final LoaderManager loaderManager = getLoaderManager(); 143 144 // Ensure that we don't try to restart a loader that doesn't exist. This becomes 145 // an issue because SuggestionLoaderCallbacks.onCreateLoader can return null 146 // as a loader if we don't have a suggestClient available yet. 147 if (loaderManager.getLoader(LOADER_ID_SUGGESTION) == null) { 148 loaderManager.initLoader(LOADER_ID_SUGGESTION, args, suggestionLoaderCallbacks); 149 } else { 150 loaderManager.restartLoader(LOADER_ID_SUGGESTION, args, suggestionLoaderCallbacks); 151 } 152 } 153 154 public static class Suggestion { 155 156 public final String value; 157 public final SpannableString display; 158 public final ForegroundColorSpan colorSpan; 159 Suggestion(String value, String searchTerm, int suggestionHighlightColor)160 public Suggestion(String value, String searchTerm, int suggestionHighlightColor) { 161 this.value = value; 162 163 display = new SpannableString(value); 164 165 colorSpan = new ForegroundColorSpan(suggestionHighlightColor); 166 167 // Highlight mixed-case matches. 168 final int start = value.toLowerCase().indexOf(searchTerm.toLowerCase()); 169 if (start >= 0) { 170 display.setSpan(colorSpan, start, start + searchTerm.length(), 0); 171 } 172 } 173 } 174 175 private class SuggestionLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<Suggestion>> { 176 @Override onCreateLoader(int id, Bundle args)177 public Loader<List<Suggestion>> onCreateLoader(int id, Bundle args) { 178 // We drop the user's search if suggestclient isn't ready. This happens if the 179 // user is really fast and starts typing before we can read shared prefs. 180 if (suggestClient != null) { 181 return new SuggestionAsyncLoader(getActivity(), suggestClient, args.getString(KEY_SEARCH_TERM)); 182 } 183 Log.e(LOG_TAG, "Autocomplete setup failed; suggestClient not ready yet."); 184 return null; 185 } 186 187 @Override onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> suggestions)188 public void onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> suggestions) { 189 // Only show the ListView if there are suggestions in it. 190 if (suggestions.size() > 0) { 191 autoCompleteAdapter.update(suggestions); 192 suggestionsList.setVisibility(View.VISIBLE); 193 } else { 194 suggestionsList.setVisibility(View.INVISIBLE); 195 } 196 } 197 198 @Override onLoaderReset(Loader<List<Suggestion>> loader)199 public void onLoaderReset(Loader<List<Suggestion>> loader) { } 200 } 201 202 private static class SuggestionAsyncLoader extends AsyncTaskLoader<List<Suggestion>> { 203 private final SuggestClient suggestClient; 204 private final String searchTerm; 205 private List<Suggestion> suggestions; 206 private final int suggestionHighlightColor; 207 SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm)208 public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) { 209 super(context); 210 this.suggestClient = suggestClient; 211 this.searchTerm = searchTerm; 212 this.suggestions = null; 213 214 // Color of search term match in search suggestion 215 suggestionHighlightColor = ContextCompat.getColor(context, R.color.suggestion_highlight); 216 } 217 218 @Override loadInBackground()219 public List<Suggestion> loadInBackground() { 220 final List<String> values = suggestClient.query(searchTerm); 221 222 final List<Suggestion> result = new ArrayList<Suggestion>(values.size()); 223 for (String value : values) { 224 result.add(new Suggestion(value, searchTerm, suggestionHighlightColor)); 225 } 226 227 return result; 228 } 229 230 @Override deliverResult(List<Suggestion> suggestions)231 public void deliverResult(List<Suggestion> suggestions) { 232 this.suggestions = suggestions; 233 234 if (isStarted()) { 235 super.deliverResult(suggestions); 236 } 237 } 238 239 @Override onStartLoading()240 protected void onStartLoading() { 241 if (suggestions != null) { 242 deliverResult(suggestions); 243 } 244 245 if (takeContentChanged() || suggestions == null) { 246 forceLoad(); 247 } 248 } 249 250 @Override onStopLoading()251 protected void onStopLoading() { 252 cancelLoad(); 253 } 254 255 @Override onReset()256 protected void onReset() { 257 super.onReset(); 258 259 onStopLoading(); 260 suggestions = null; 261 } 262 } 263 } 264