1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
|
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.server.search.SearchableInfo;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
import java.io.FileNotFoundException;
import java.util.WeakHashMap;
/**
* Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
*
* @hide
*/
class SuggestionsAdapter extends ResourceCursorAdapter {
// The value used to query a cursor whether it is still expecting more input,
// so we can correctly display (or not display) the 'working' spinner in the search dialog.
public static final String IS_WORKING = "isWorking";
private static final boolean DBG = false;
private static final String LOG_TAG = "SuggestionsAdapter";
private SearchDialog mSearchDialog;
private SearchableInfo mSearchable;
private Context mProviderContext;
private WeakHashMap<String, Drawable> mOutsideDrawablesCache;
private boolean mGlobalSearchMode;
// Cached column indexes, updated when the cursor changes.
private int mFormatCol;
private int mText1Col;
private int mText2Col;
private int mIconName1Col;
private int mIconName2Col;
private int mIconBitmap1Col;
private int mIconBitmap2Col;
public SuggestionsAdapter(Context context, SearchDialog searchDialog, SearchableInfo searchable,
WeakHashMap<String, Drawable> outsideDrawablesCache, boolean globalSearchMode) {
super(context,
com.android.internal.R.layout.search_dropdown_item_icons_2line,
null, // no initial cursor
true); // auto-requery
mSearchDialog = searchDialog;
mSearchable = searchable;
// set up provider resources (gives us icons, etc.)
Context activityContext = mSearchable.getActivityContext(mContext);
mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
mOutsideDrawablesCache = outsideDrawablesCache;
mGlobalSearchMode = globalSearchMode;
}
/**
* Overridden to always return <code>false</code>, since we cannot be sure that
* suggestion sources return stable IDs.
*/
@Override
public boolean hasStableIds() {
return false;
}
/**
* Use the search suggestions provider to obtain a live cursor. This will be called
* in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
* The results will be processed in the UI thread and changeCursor() will be called.
*/
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
String query = (constraint == null) ? "" : constraint.toString();
try {
return SearchManager.getSuggestions(mContext, mSearchable, query);
} catch (RuntimeException e) {
Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
return null;
}
}
/**
* Cache columns.
*/
@Override
public void changeCursor(Cursor c) {
if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
super.changeCursor(c);
if (c != null) {
mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT);
mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP);
mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP);
}
updateWorking();
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
updateWorking();
}
/**
* Updates the search dialog according to the current working status of the cursor.
*/
private void updateWorking() {
if (!mGlobalSearchMode || mCursor == null) return;
Bundle request = new Bundle();
request.putString(SearchManager.EXTRA_DATA_KEY, IS_WORKING);
Bundle response = mCursor.respond(request);
if (response.containsKey(IS_WORKING)) {
boolean isWorking = response.getBoolean(IS_WORKING);
mSearchDialog.setWorking(isWorking);
}
}
/**
* Tags the view with cached child view look-ups.
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
View v = super.newView(context, cursor, parent);
v.setTag(new ChildViewCache(v));
return v;
}
/**
* Cache of the child views of drop-drown list items, to avoid looking up the children
* each time the contents of a list item are changed.
*/
private final static class ChildViewCache {
public final TextView mText1;
public final TextView mText2;
public final ImageView mIcon1;
public final ImageView mIcon2;
public ChildViewCache(View v) {
mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1);
mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2);
mIcon1 = (ImageView) v.findViewById(com.android.internal.R.id.icon1);
mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2);
}
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ChildViewCache views = (ChildViewCache) view.getTag();
boolean isHtml = false;
if (mFormatCol >= 0) {
String format = cursor.getString(mFormatCol);
isHtml = "html".equals(format);
}
setViewText(cursor, views.mText1, mText1Col, isHtml);
setViewText(cursor, views.mText2, mText2Col, isHtml);
setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col);
setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col);
}
private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) {
if (v == null) {
return;
}
CharSequence text = null;
if (textCol >= 0) {
String str = cursor.getString(textCol);
text = (str != null && isHtml) ? Html.fromHtml(str) : str;
}
// Set the text even if it's null, since we need to clear any previous text.
v.setText(text);
if (TextUtils.isEmpty(text)) {
v.setVisibility(View.GONE);
} else {
v.setVisibility(View.VISIBLE);
}
}
private void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) {
if (v == null) {
return;
}
Drawable drawable = null;
// First try the bitmap column
if (iconBitmapCol >= 0) {
byte[] data = cursor.getBlob(iconBitmapCol);
if (data != null) {
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap != null) {
drawable = new BitmapDrawable(bitmap);
}
}
}
// If there was no bitmap, try the icon resource column.
if (drawable == null && iconNameCol >= 0) {
String value = cursor.getString(iconNameCol);
drawable = getDrawableFromResourceValue(value);
}
// Set the icon even if the drawable is null, since we need to clear any
// previous icon.
v.setImageDrawable(drawable);
if (drawable == null) {
v.setVisibility(View.GONE);
} else {
v.setVisibility(View.VISIBLE);
}
}
/**
* Gets the text to show in the query field when a suggestion is selected.
*
* @param cursor The Cursor to read the suggestion data from. The Cursor should already
* be moved to the suggestion that is to be read from.
* @return The text to show, or <code>null</code> if the query should not be
* changed when selecting this suggestion.
*/
@Override
public CharSequence convertToString(Cursor cursor) {
if (cursor == null) {
return null;
}
String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
if (query != null) {
return query;
}
if (mSearchable.shouldRewriteQueryFromData()) {
String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data != null) {
return data;
}
}
if (mSearchable.shouldRewriteQueryFromText()) {
String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
if (text1 != null) {
return text1;
}
}
return null;
}
/**
* This method is overridden purely to provide a bit of protection against
* flaky content providers.
*
* @see android.widget.ListAdapter#getView(int, View, ViewGroup)
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
try {
return super.getView(position, convertView, parent);
} catch (RuntimeException e) {
Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
// Put exception string in item title
View v = newView(mContext, mCursor, parent);
if (v != null) {
ChildViewCache views = (ChildViewCache) v.getTag();
TextView tv = views.mText1;
tv.setText(e.toString());
}
return v;
}
}
/**
* Gets a drawable given a value provided by a suggestion provider.
*
* This value could be just the string value of a resource id
* (e.g., "2130837524"), in which case we will try to retrieve a drawable from
* the provider's resources. If the value is not an integer, it is
* treated as a Uri and opened with
* {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
*
* All resources and URIs are read using the suggestion provider's context.
*
* If the string is not formatted as expected, or no drawable can be found for
* the provided value, this method returns null.
*
* @param drawableId a string like "2130837524",
* "android.resource://com.android.alarmclock/2130837524",
* or "content://contacts/photos/253".
* @return a Drawable, or null if none found
*/
private Drawable getDrawableFromResourceValue(String drawableId) {
if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
return null;
}
// First, check the cache.
Drawable drawable = mOutsideDrawablesCache.get(drawableId);
if (drawable != null) return drawable;
try {
// Not cached, try using it as a plain resource ID in the provider's context.
int resourceId = Integer.parseInt(drawableId);
drawable = mProviderContext.getResources().getDrawable(resourceId);
} catch (NumberFormatException nfe) {
// The id was not an integer resource id.
// Let the ContentResolver handle content, android.resource and file URIs.
try {
Uri uri = Uri.parse(drawableId);
drawable = Drawable.createFromStream(
mProviderContext.getContentResolver().openInputStream(uri),
null);
} catch (FileNotFoundException fnfe) {
// drawable = null;
}
// If we got a drawable for this resource id, then stick it in the
// map so we don't do this lookup again.
if (drawable != null) {
mOutsideDrawablesCache.put(drawableId, drawable);
}
} catch (NotFoundException nfe) {
// Resource could not be found
// drawable = null;
}
return drawable;
}
/**
* Gets the value of a string column by name.
*
* @param cursor Cursor to read the value from.
* @param columnName The name of the column to read.
* @return The value of the given column, or <code>null</null>
* if the cursor does not contain the given column.
*/
public static String getColumnString(Cursor cursor, String columnName) {
int col = cursor.getColumnIndex(columnName);
if (col == -1) {
return null;
}
return cursor.getString(col);
}
}
|