DefaultSuggestionView.java revision fb8ce18922dae59db424fce906b5c113797fe81e
1/* 2 * Copyright (C) 2009 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 17package com.android.quicksearchbox.ui; 18 19import com.android.quicksearchbox.QsbApplication; 20import com.android.quicksearchbox.R; 21import com.android.quicksearchbox.Source; 22import com.android.quicksearchbox.Suggestion; 23import com.android.quicksearchbox.SuggestionFormatter; 24import com.android.quicksearchbox.util.Consumer; 25import com.android.quicksearchbox.util.NowOrLater; 26 27import android.content.Context; 28import android.content.res.ColorStateList; 29import android.graphics.drawable.Drawable; 30import android.text.Html; 31import android.text.Spannable; 32import android.text.SpannableString; 33import android.text.TextUtils; 34import android.text.style.TextAppearanceSpan; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.view.ContextMenu; 38import android.view.KeyEvent; 39import android.view.MenuInflater; 40import android.view.MenuItem; 41import android.view.View; 42import android.widget.ImageView; 43import android.widget.RelativeLayout; 44import android.widget.TextView; 45 46/** 47 * View for the items in the suggestions list. This includes promoted suggestions, 48 * sources, and suggestions under each source. 49 */ 50public class DefaultSuggestionView extends RelativeLayout implements SuggestionView { 51 52 private static final boolean DBG = false; 53 54 public static final String VIEW_ID = "default"; 55 56 private static int sId = 0; 57 // give the TAG an unique ID to make debugging easier (there are lots of these!) 58 private final String TAG = "QSB.SuggestionView:" + (sId++); 59 60 private TextView mText1; 61 private TextView mText2; 62 private AsyncIcon mIcon1; 63 private AsyncIcon mIcon2; 64 private final SuggestionFormatter mSuggestionFormatter; 65 private boolean mIsFromHistory; 66 private boolean mRefineable; 67 private int mPosition; 68 private SuggestionsAdapter mAdapter; 69 private KeyListener mKeyListener; 70 71 public DefaultSuggestionView(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 mSuggestionFormatter = QsbApplication.get(context).getSuggestionFormatter(); 74 } 75 76 public DefaultSuggestionView(Context context, AttributeSet attrs) { 77 super(context, attrs); 78 mSuggestionFormatter = QsbApplication.get(context).getSuggestionFormatter(); 79 } 80 81 public DefaultSuggestionView(Context context) { 82 super(context); 83 mSuggestionFormatter = QsbApplication.get(context).getSuggestionFormatter(); 84 } 85 86 @Override 87 protected void onFinishInflate() { 88 super.onFinishInflate(); 89 mText1 = (TextView) findViewById(R.id.text1); 90 mText2 = (TextView) findViewById(R.id.text2); 91 mIcon1 = new AsyncIcon((ImageView) findViewById(R.id.icon1)){ 92 // override default icon (when no other available) with default source icon 93 @Override 94 protected String getFallbackIconId(Source source) { 95 return source.getSourceIconUri().toString(); 96 } 97 @Override 98 protected Drawable getFallbackIcon(Source source) { 99 return source.getSourceIcon(); 100 } 101 }; 102 mIcon2 = new AsyncIcon((ImageView) findViewById(R.id.icon2)); 103 // for some reason, creating mKeyListener inside the constructor causes it not to work. 104 mKeyListener = new KeyListener(); 105 106 setOnKeyListener(mKeyListener); 107 } 108 109 public void bindAsSuggestion(Suggestion suggestion, String userQuery) { 110 setOnClickListener(new ClickListener()); 111 112 CharSequence text1 = formatText(suggestion.getSuggestionText1(), suggestion, userQuery); 113 CharSequence text2 = suggestion.getSuggestionText2Url(); 114 if (text2 != null) { 115 text2 = formatUrl(text2); 116 } else { 117 text2 = formatText(suggestion.getSuggestionText2(), suggestion, null); 118 } 119 // If there is no text for the second line, allow the first line to be up to two lines 120 if (TextUtils.isEmpty(text2)) { 121 mText1.setSingleLine(false); 122 mText1.setMaxLines(2); 123 mText1.setEllipsize(TextUtils.TruncateAt.START); 124 } else { 125 mText1.setSingleLine(true); 126 mText1.setMaxLines(1); 127 mText1.setEllipsize(TextUtils.TruncateAt.MIDDLE); 128 } 129 setText1(text1); 130 setText2(text2); 131 mIcon1.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon1()); 132 mIcon2.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon2()); 133 updateIsFromHistory(suggestion); 134 updateRefinable(suggestion); 135 136 setLongClickable(needsContextMenu()); 137 138 if (DBG) { 139 Log.d(TAG, "bindAsSuggestion(), text1=" + text1 + ",text2=" + text2 + ",q='" + 140 userQuery + "',refinable=" + mRefineable + ",fromHistory=" + mIsFromHistory); 141 } 142 } 143 144 public void bindAdapter(SuggestionsAdapter adapter, int position) { 145 mAdapter = adapter; 146 mPosition = position; 147 } 148 149 protected boolean needsContextMenu() { 150 return isFromHistory(); 151 } 152 153 protected boolean isFromHistory() { 154 return mIsFromHistory; 155 } 156 157 protected void updateIsFromHistory(Suggestion suggestion) { 158 mIsFromHistory = suggestion.isSuggestionShortcut() || suggestion.isHistorySuggestion(); 159 } 160 161 protected void updateRefinable(Suggestion suggestion) { 162 mRefineable = 163 suggestion.isWebSearchSuggestion() 164 && mIcon2.getView().getDrawable() == null 165 && !TextUtils.isEmpty(suggestion.getSuggestionQuery()); 166 if (DBG) Log.d(TAG, "updateRefinable: " + mRefineable); 167 setRefinable(suggestion, mRefineable); 168 } 169 170 protected void setRefinable(Suggestion suggestion, boolean refinable) { 171 if (refinable) { 172 mIcon2.getView().setOnClickListener(new View.OnClickListener() { 173 public void onClick(View v) { 174 onSuggestionQueryRefineClicked(); 175 } 176 }); 177 mIcon2.getView().setFocusable(true); 178 mIcon2.getView().setOnKeyListener(mKeyListener); 179 Drawable icon2 = getContext().getResources().getDrawable(R.drawable.edit_query); 180 Drawable background = 181 getContext().getResources().getDrawable(R.drawable.edit_query_background); 182 mIcon2.setDrawable(icon2, background, String.valueOf(R.drawable.edit_query)); 183 } else { 184 mIcon2.getView().setOnClickListener(null); 185 mIcon2.getView().setFocusable(false); 186 mIcon2.getView().setOnKeyListener(null); 187 } 188 } 189 190 private CharSequence formatUrl(CharSequence url) { 191 SpannableString text = new SpannableString(url); 192 ColorStateList colors = getResources().getColorStateList(R.color.url_text); 193 text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), 194 0, url.length(), 195 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 196 return text; 197 } 198 199 private CharSequence formatText(String str, Suggestion suggestion, 200 String userQuery) { 201 boolean isHtml = "html".equals(suggestion.getSuggestionFormat()); 202 if (isHtml && looksLikeHtml(str)) { 203 return Html.fromHtml(str); 204 } else if (suggestion.isWebSearchSuggestion() && !TextUtils.isEmpty(userQuery)) { 205 return mSuggestionFormatter.formatSuggestion(userQuery, str); 206 } else { 207 return str; 208 } 209 } 210 211 private boolean looksLikeHtml(String str) { 212 if (TextUtils.isEmpty(str)) return false; 213 for (int i = str.length() - 1; i >= 0; i--) { 214 char c = str.charAt(i); 215 if (c == '>' || c == '&') return true; 216 } 217 return false; 218 } 219 220 /** 221 * Sets the first text line. 222 */ 223 private void setText1(CharSequence text) { 224 mText1.setText(text); 225 } 226 227 /** 228 * Sets the second text line. 229 */ 230 private void setText2(CharSequence text) { 231 mText2.setText(text); 232 if (TextUtils.isEmpty(text)) { 233 mText2.setVisibility(GONE); 234 } else { 235 mText2.setVisibility(VISIBLE); 236 } 237 } 238 239 /** 240 * Sets the drawable in an image view, makes sure the view is only visible if there 241 * is a drawable. 242 */ 243 private static void setViewDrawable(ImageView v, Drawable drawable) { 244 // Set the icon even if the drawable is null, since we need to clear any 245 // previous icon. 246 v.setImageDrawable(drawable); 247 248 if (drawable == null) { 249 v.setVisibility(View.GONE); 250 } else { 251 v.setVisibility(View.VISIBLE); 252 253 // This is a hack to get any animated drawables (like a 'working' spinner) 254 // to animate. You have to setVisible true on an AnimationDrawable to get 255 // it to start animating, but it must first have been false or else the 256 // call to setVisible will be ineffective. We need to clear up the story 257 // about animated drawables in the future, see http://b/1878430. 258 drawable.setVisible(false, false); 259 drawable.setVisible(true, false); 260 } 261 } 262 263 @Override 264 protected void onCreateContextMenu(ContextMenu menu) { 265 super.onCreateContextMenu(menu); 266 if (isFromHistory()) { 267 MenuInflater inflater = new MenuInflater(getContext()); 268 inflater.inflate(R.menu.remove_from_history, menu); 269 MenuItem removeFromHistory = menu.findItem(R.id.remove_from_history); 270 removeFromHistory.setOnMenuItemClickListener(new RemoveFromHistoryListener()); 271 } 272 } 273 274 protected void onSuggestionClicked() { 275 if (mAdapter != null) { 276 mAdapter.onSuggestionClicked(mPosition); 277 } 278 } 279 280 protected void onSuggestionQuickContactClicked() { 281 if (mAdapter != null) { 282 mAdapter.onSuggestionQuickContactClicked(mPosition); 283 } 284 } 285 286 protected void onRemoveFromHistoryClicked() { 287 if (mAdapter != null) { 288 mAdapter.onSuggestionRemoveFromHistoryClicked(mPosition); 289 } 290 } 291 292 protected void onSuggestionQueryRefineClicked() { 293 if (mAdapter != null) { 294 mAdapter.onSuggestionQueryRefineClicked(mPosition); 295 } 296 } 297 298 private class ClickListener implements OnClickListener { 299 public void onClick(View v) { 300 onSuggestionClicked(); 301 } 302 } 303 304 private class RemoveFromHistoryListener implements MenuItem.OnMenuItemClickListener { 305 public boolean onMenuItemClick(MenuItem item) { 306 onRemoveFromHistoryClicked(); 307 return false; 308 } 309 } 310 311 private class KeyListener implements View.OnKeyListener { 312 public boolean onKey(View v, int keyCode, KeyEvent event) { 313 boolean consumed = false; 314 if (event.getAction() == KeyEvent.ACTION_DOWN) { 315 if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && v != mIcon2.getView()) { 316 consumed = mIcon2.getView().requestFocus(); 317 if (DBG) Log.d(TAG, "onKey Icon2 accepted focus: " + consumed); 318 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && v == mIcon2.getView()) { 319 consumed = requestFocus(); 320 if (DBG) Log.d(TAG, "onKey SuggestionView accepted focus: " + consumed); 321 } 322 } 323 return consumed; 324 } 325 } 326 327 private class AsyncIcon { 328 private final ImageView mView; 329 private String mCurrentId; 330 private String mWantedId; 331 332 public AsyncIcon(ImageView view) { 333 mView = view; 334 } 335 336 public void set(final Source source, final String iconId) { 337 if (iconId != null) { 338 mWantedId = iconId; 339 if (!TextUtils.equals(mWantedId, mCurrentId)) { 340 if (DBG) Log.d(TAG, "getting icon Id=" + iconId); 341 NowOrLater<Drawable> icon = source.getIcon(iconId); 342 if (icon.haveNow()) { 343 if (DBG) Log.d(TAG, "getIcon ready now"); 344 handleNewDrawable(icon.getNow(), iconId, source); 345 } else { 346 // make sure old icon is not visible while new one is loaded 347 if (DBG) Log.d(TAG , "getIcon getting later"); 348 clearDrawable(); 349 icon.getLater(new Consumer<Drawable>(){ 350 public boolean consume(Drawable icon) { 351 if (DBG) { 352 Log.d(TAG, "IconConsumer.consume got id " + iconId + 353 " want id " + mWantedId); 354 } 355 // ensure we have not been re-bound since the request was made. 356 if (TextUtils.equals(iconId, mWantedId)) { 357 handleNewDrawable(icon, iconId, source); 358 return true; 359 } 360 return false; 361 }}); 362 } 363 } 364 } else { 365 mWantedId = null; 366 handleNewDrawable(null, null, source); 367 } 368 } 369 370 public ImageView getView() { 371 return mView; 372 } 373 374 private void handleNewDrawable(Drawable icon, String id, Source source) { 375 if (icon == null) { 376 mWantedId = getFallbackIconId(source); 377 if (TextUtils.equals(mWantedId, mCurrentId)) { 378 return; 379 } 380 icon = getFallbackIcon(source); 381 } 382 setDrawable(icon, id); 383 } 384 385 public void setDrawable(Drawable icon, Drawable background, String id) { 386 mCurrentId = id; 387 mWantedId = id; 388 setViewDrawable(mView, icon); 389 mView.setBackgroundDrawable(background); 390 } 391 392 private void setDrawable(Drawable icon, String id) { 393 mCurrentId = id; 394 setViewDrawable(mView, icon); 395 } 396 397 private void clearDrawable() { 398 mCurrentId = null; 399 mView.setImageDrawable(null); 400 } 401 402 protected String getFallbackIconId(Source source) { 403 return null; 404 } 405 406 protected Drawable getFallbackIcon(Source source) { 407 return null; 408 } 409 410 } 411} 412