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