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 
17 package com.android.quicksearchbox.ui;
18 
19 import com.android.quicksearchbox.R;
20 import com.android.quicksearchbox.Source;
21 import com.android.quicksearchbox.Suggestion;
22 import com.android.quicksearchbox.util.Consumer;
23 import com.android.quicksearchbox.util.NowOrLater;
24 
25 import android.content.Context;
26 import android.content.res.ColorStateList;
27 import android.graphics.drawable.Drawable;
28 import android.net.Uri;
29 import android.text.Html;
30 import android.text.Spannable;
31 import android.text.SpannableString;
32 import android.text.TextUtils;
33 import android.text.style.TextAppearanceSpan;
34 import android.util.AttributeSet;
35 import android.util.Log;
36 import android.view.View;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 
40 /**
41  * View for the items in the suggestions list. This includes promoted suggestions,
42  * sources, and suggestions under each source.
43  */
44 public class DefaultSuggestionView extends BaseSuggestionView {
45 
46     private static final boolean DBG = false;
47 
48     private static final String VIEW_ID = "default";
49 
50     private final String TAG = "QSB.DefaultSuggestionView";
51 
52     private AsyncIcon mAsyncIcon1;
53     private AsyncIcon mAsyncIcon2;
54 
DefaultSuggestionView(Context context, AttributeSet attrs, int defStyle)55     public DefaultSuggestionView(Context context, AttributeSet attrs, int defStyle) {
56         super(context, attrs, defStyle);
57     }
58 
DefaultSuggestionView(Context context, AttributeSet attrs)59     public DefaultSuggestionView(Context context, AttributeSet attrs) {
60         super(context, attrs);
61     }
62 
DefaultSuggestionView(Context context)63     public DefaultSuggestionView(Context context) {
64         super(context);
65     }
66 
67     @Override
onFinishInflate()68     protected void onFinishInflate() {
69         super.onFinishInflate();
70         mText1 = (TextView) findViewById(R.id.text1);
71         mText2 = (TextView) findViewById(R.id.text2);
72         mAsyncIcon1 = new AsyncIcon(mIcon1) {
73             // override default icon (when no other available) with default source icon
74             @Override
75             protected String getFallbackIconId(Source source) {
76                 return source.getSourceIconUri().toString();
77             }
78             @Override
79             protected Drawable getFallbackIcon(Source source) {
80                 return source.getSourceIcon();
81             }
82         };
83         mAsyncIcon2 = new AsyncIcon(mIcon2);
84     }
85 
86     @Override
bindAsSuggestion(Suggestion suggestion, String userQuery)87     public void bindAsSuggestion(Suggestion suggestion, String userQuery) {
88         super.bindAsSuggestion(suggestion, userQuery);
89 
90         CharSequence text1 = formatText(suggestion.getSuggestionText1(), suggestion);
91         CharSequence text2 = suggestion.getSuggestionText2Url();
92         if (text2 != null) {
93             text2 = formatUrl(text2);
94         } else {
95             text2 = formatText(suggestion.getSuggestionText2(), suggestion);
96         }
97         // If there is no text for the second line, allow the first line to be up to two lines
98         if (TextUtils.isEmpty(text2)) {
99             mText1.setSingleLine(false);
100             mText1.setMaxLines(2);
101             mText1.setEllipsize(TextUtils.TruncateAt.START);
102         } else {
103             mText1.setSingleLine(true);
104             mText1.setMaxLines(1);
105             mText1.setEllipsize(TextUtils.TruncateAt.MIDDLE);
106         }
107         setText1(text1);
108         setText2(text2);
109         mAsyncIcon1.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon1());
110         mAsyncIcon2.set(suggestion.getSuggestionSource(), suggestion.getSuggestionIcon2());
111 
112         if (DBG) {
113             Log.d(TAG, "bindAsSuggestion(), text1=" + text1 + ",text2=" + text2 + ",q='" +
114                     userQuery + ",fromHistory=" + isFromHistory(suggestion));
115         }
116     }
117 
formatUrl(CharSequence url)118     private CharSequence formatUrl(CharSequence url) {
119         SpannableString text = new SpannableString(url);
120         ColorStateList colors = getResources().getColorStateList(R.color.url_text);
121         text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null),
122                 0, url.length(),
123                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
124         return text;
125     }
126 
formatText(String str, Suggestion suggestion)127     private CharSequence formatText(String str, Suggestion suggestion) {
128         boolean isHtml = "html".equals(suggestion.getSuggestionFormat());
129         if (isHtml && looksLikeHtml(str)) {
130             return Html.fromHtml(str);
131         } else {
132             return str;
133         }
134     }
135 
looksLikeHtml(String str)136     private boolean looksLikeHtml(String str) {
137         if (TextUtils.isEmpty(str)) return false;
138         for (int i = str.length() - 1; i >= 0; i--) {
139             char c = str.charAt(i);
140             if (c == '>' || c == '&') return true;
141         }
142         return false;
143     }
144 
145     /**
146      * Sets the drawable in an image view, makes sure the view is only visible if there
147      * is a drawable.
148      */
setViewDrawable(ImageView v, Drawable drawable)149     private static void setViewDrawable(ImageView v, Drawable drawable) {
150         // Set the icon even if the drawable is null, since we need to clear any
151         // previous icon.
152         v.setImageDrawable(drawable);
153 
154         if (drawable == null) {
155             v.setVisibility(View.GONE);
156         } else {
157             v.setVisibility(View.VISIBLE);
158 
159             // This is a hack to get any animated drawables (like a 'working' spinner)
160             // to animate. You have to setVisible true on an AnimationDrawable to get
161             // it to start animating, but it must first have been false or else the
162             // call to setVisible will be ineffective. We need to clear up the story
163             // about animated drawables in the future, see http://b/1878430.
164             drawable.setVisible(false, false);
165             drawable.setVisible(true, false);
166         }
167     }
168 
169     private class AsyncIcon {
170         private final ImageView mView;
171         private String mCurrentId;
172         private String mWantedId;
173 
AsyncIcon(ImageView view)174         public AsyncIcon(ImageView view) {
175             mView = view;
176         }
177 
set(final Source source, final String sourceIconId)178         public void set(final Source source, final String sourceIconId) {
179             if (sourceIconId != null) {
180                 // The iconId can just be a package-relative resource ID, which may overlap with
181                 // other packages. Make sure it's globally unique.
182                 Uri iconUri = source.getIconUri(sourceIconId);
183                 final String uniqueIconId = iconUri == null ? null : iconUri.toString();
184                 mWantedId = uniqueIconId;
185                 if (!TextUtils.equals(mWantedId, mCurrentId)) {
186                     if (DBG) Log.d(TAG, "getting icon Id=" + uniqueIconId);
187                     NowOrLater<Drawable> icon = source.getIcon(sourceIconId);
188                     if (icon.haveNow()) {
189                         if (DBG) Log.d(TAG, "getIcon ready now");
190                         handleNewDrawable(icon.getNow(), uniqueIconId, source);
191                     } else {
192                         // make sure old icon is not visible while new one is loaded
193                         if (DBG) Log.d(TAG , "getIcon getting later");
194                         clearDrawable();
195                         icon.getLater(new Consumer<Drawable>(){
196                             @Override
197                             public boolean consume(Drawable icon) {
198                                 if (DBG) {
199                                     Log.d(TAG, "IconConsumer.consume got id " + uniqueIconId +
200                                             " want id " + mWantedId);
201                                 }
202                                 // ensure we have not been re-bound since the request was made.
203                                 if (TextUtils.equals(uniqueIconId, mWantedId)) {
204                                     handleNewDrawable(icon, uniqueIconId, source);
205                                     return true;
206                                 }
207                                 return false;
208                             }});
209                     }
210                 }
211             } else {
212                 mWantedId = null;
213                 handleNewDrawable(null, null, source);
214             }
215         }
216 
handleNewDrawable(Drawable icon, String id, Source source)217         private void handleNewDrawable(Drawable icon, String id, Source source) {
218             if (icon == null) {
219                 mWantedId = getFallbackIconId(source);
220                 if (TextUtils.equals(mWantedId, mCurrentId)) {
221                     return;
222                 }
223                 icon = getFallbackIcon(source);
224             }
225             setDrawable(icon, id);
226         }
227 
setDrawable(Drawable icon, String id)228         private void setDrawable(Drawable icon, String id) {
229             mCurrentId = id;
230             setViewDrawable(mView, icon);
231         }
232 
clearDrawable()233         private void clearDrawable() {
234             mCurrentId = null;
235             mView.setImageDrawable(null);
236         }
237 
getFallbackIconId(Source source)238         protected String getFallbackIconId(Source source) {
239             return null;
240         }
241 
getFallbackIcon(Source source)242         protected Drawable getFallbackIcon(Source source) {
243             return null;
244         }
245 
246     }
247 
248     public static class Factory extends SuggestionViewInflater {
Factory(Context context)249         public Factory(Context context) {
250             super(VIEW_ID, DefaultSuggestionView.class, R.layout.suggestion, context);
251         }
252     }
253 
254 }
255