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