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