SearchActivity.java revision f1ab045ff991e1ec3e4161d213627b8629971c0e
1/** 2 * Copyright (c) 2009, Google Inc. 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.mms.ui; 18 19import com.android.mms.MmsApp; 20import com.android.mms.R; 21import android.app.ListActivity; 22import android.app.SearchManager; 23import android.content.AsyncQueryHandler; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.Intent; 27import android.database.Cursor; 28import android.graphics.Color; 29import android.graphics.Typeface; 30import android.net.Uri; 31import android.os.Bundle; 32import android.provider.Telephony; 33import android.text.SpannableString; 34import android.text.TextPaint; 35import android.text.style.ForegroundColorSpan; 36import android.text.style.StyleSpan; 37import android.util.AttributeSet; 38import android.view.LayoutInflater; 39import android.view.View; 40import android.view.ViewGroup; 41import android.widget.CursorAdapter; 42import android.widget.ListView; 43import android.widget.TextView; 44 45import com.android.mms.data.Contact; 46import com.android.mms.ui.ComposeMessageActivity; 47 48/*** 49 * Presents a List of search results. Each item in the list represents a thread which 50 * matches. The item contains the contact (or phone number) as the "title" and a 51 * snippet of what matches, below. The snippet is taken from the most recent part of 52 * the conversation that has a match. Each match within the visible portion of the 53 * snippet is highlighted. 54 */ 55 56public class SearchActivity extends ListActivity 57{ 58 AsyncQueryHandler mQueryHandler; 59 60 /* 61 * Subclass of TextView which displays a snippet of text which matches the full text and 62 * highlights the matches within the snippet. 63 */ 64 public static class TextViewSnippet extends TextView { 65 private static String sEllipsis = "\u2026"; 66 67 private static int sTypefaceHighlight = Typeface.BOLD; 68 69 private String mFullText; 70 private String mTargetString; 71 72 public TextViewSnippet(Context context, AttributeSet attrs) { 73 super(context, attrs); 74 } 75 76 public TextViewSnippet(Context context) { 77 super(context); 78 } 79 80 public TextViewSnippet(Context context, AttributeSet attrs, int defStyle) { 81 super(context, attrs, defStyle); 82 } 83 84 /** 85 * We have to know our width before we can compute the snippet string. Do that 86 * here and then defer to super for whatever work is normally done. 87 */ 88 @Override 89 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 90 String fullTextLower = mFullText.toLowerCase(); 91 String targetStringLower = mTargetString.toLowerCase(); 92 93 int startPos = fullTextLower.indexOf(targetStringLower); 94 int searchStringLength = targetStringLower.length(); 95 int bodyLength = fullTextLower.length(); 96 97 TextPaint tp = getPaint(); 98 99 float searchStringWidth = tp.measureText(mTargetString); 100 float textFieldWidth = getWidth(); 101 102 String snippetString = null; 103 if (searchStringWidth > textFieldWidth) { 104 snippetString = mFullText.substring(startPos, startPos + searchStringLength); 105 } else { 106 float ellipsisWidth = tp.measureText(sEllipsis); 107 textFieldWidth -= (2F * ellipsisWidth); // assume we'll need one on both ends 108 109 int offset = -1; 110 int start = -1; 111 int end = -1; 112 /* TODO: this code could be made more efficient by only measuring the additional 113 * characters as we widen the string rather than measuring the whole new 114 * string each time. 115 */ 116 while (true) { 117 offset += 1; 118 119 int newstart = Math.max(0, startPos - offset); 120 int newend = Math.min(bodyLength, startPos + searchStringLength + offset); 121 122 if (newstart == start && newend == end) { 123 // if we couldn't expand out any further then we're done 124 break; 125 } 126 start = newstart; 127 end = newend; 128 129 // pull the candidate string out of the full text rather than body 130 // because body has been toLower()'ed 131 String candidate = mFullText.substring(start, end); 132 if (tp.measureText(candidate) > textFieldWidth) { 133 // if the newly computed width would exceed our bounds then we're done 134 // do not use this "candidate" 135 break; 136 } 137 138 snippetString = String.format( 139 "%s%s%s", 140 start == 0 ? "" : sEllipsis, 141 candidate, 142 end == bodyLength ? "" : sEllipsis); 143 } 144 } 145 146 String snippetStringLower = snippetString.toLowerCase(); 147 SpannableString spannable = new SpannableString(snippetString); 148 int start = 0; 149 while (true) { 150 int index = snippetStringLower.indexOf(targetStringLower, start); 151 if (index == -1) { 152 break; 153 } 154// spannable.setSpan(new ForegroundColorSpan(sHighlightColor), index, index+searchStringLength, 0); 155 spannable.setSpan(new StyleSpan(sTypefaceHighlight), index, index+searchStringLength, 0); 156 start = index + searchStringLength; 157 } 158 setText(spannable); 159 160 // do this after the call to setText() above 161 super.onLayout(changed, left, top, right, bottom); 162 } 163 164 public void setText(String fullText, String target) { 165 mFullText = fullText; 166 mTargetString = target; 167 requestLayout(); 168 } 169 } 170 171 public void onCreate(Bundle icicle) 172 { 173 super.onCreate(icicle); 174 setContentView(R.layout.search_activity); 175 176 String searchStringParameter = getIntent().getStringExtra(SearchManager.QUERY).trim(); 177 final String searchString = 178 searchStringParameter != null ? searchStringParameter.trim() : searchStringParameter; 179 ContentResolver cr = getContentResolver(); 180 181 final ListView listView = getListView(); 182 listView.setSelector(android.R.drawable.list_selector_background); 183 listView.setItemsCanFocus(true); 184 listView.setFocusable(true); 185 listView.setClickable(true); 186 187 // I considered something like "searching..." but typically it will 188 // flash on the screen briefly which I found to be more distracting 189 // than beneficial. 190 // This gets updated when the query completes. 191 setTitle(""); 192 193 // When the query completes cons up a new adapter and set our list adapter to that. 194 mQueryHandler = new AsyncQueryHandler(cr) { 195 protected void onQueryComplete(int token, Object cookie, Cursor c) { 196 final int threadIdPos = c.getColumnIndex("thread_id"); 197 final int addressPos = c.getColumnIndex("address"); 198 final int bodyPos = c.getColumnIndex("body"); 199 final int rowidPos = c.getColumnIndex("_id"); 200 201 int cursorCount = c.getCount(); 202 setTitle(getResources().getQuantityString( 203 R.plurals.search_results_title, 204 cursorCount, 205 cursorCount, 206 searchString)); 207 208 // Note that we're telling the CursorAdapter not to do auto-requeries. If we 209 // want to dynamically respond to changes in the search results, 210 // we'll have have to add a setOnDataSetChangedListener(). 211 setListAdapter(new CursorAdapter(SearchActivity.this, 212 c, false /* no auto-requery */) { 213 @Override 214 public void bindView(View view, Context context, Cursor cursor) { 215 final TextView title = (TextView)(view.findViewById(R.id.title)); 216 final TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle)); 217 218 String address = cursor.getString(addressPos); 219 Contact contact = Contact.get(address, false); 220 221 contact.addListener(new Contact.UpdateListener() { 222 public void onUpdate(final Contact updated) { 223 runOnUiThread(new Runnable() { 224 public void run() { 225 title.setText(updated.getNameAndNumber()); 226 } 227 }); 228 } 229 }); 230 231 String titleString = contact.getNameAndNumber(); 232 title.setText(titleString); 233 234 snippet.setText(cursor.getString(bodyPos), searchString); 235 236 // if the user touches the item then launch the compose message 237 // activity with some extra parameters to highlight the search 238 // results and scroll to the latest part of the conversation 239 // that has a match. 240 final long threadId = cursor.getLong(threadIdPos); 241 final long rowid = cursor.getLong(rowidPos); 242 243 view.setOnClickListener(new View.OnClickListener() { 244 public void onClick(View v) { 245 final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class); 246 onClickIntent.putExtra("thread_id", threadId); 247 onClickIntent.putExtra("highlight", searchString); 248 onClickIntent.putExtra("select_id", rowid); 249 startActivity(onClickIntent); 250 } 251 }); 252 } 253 254 @Override 255 public View newView(Context context, Cursor cursor, ViewGroup parent) { 256 LayoutInflater inflater = LayoutInflater.from(context); 257 View v = inflater.inflate(R.layout.search_item, parent, false); 258 return v; 259 } 260 261 }); 262 263 // ListView seems to want to reject the setFocusable until such time 264 // as the list is not empty. Set it here and request focus. Without 265 // this the arrow keys (and trackball) fail to move the selection. 266 listView.setFocusable(true); 267 listView.setFocusableInTouchMode(true); 268 listView.requestFocus(); 269 270 // Remember the query if there are actual results 271 if (cursorCount > 0) { 272 MmsApp mmsApp = (MmsApp)getApplication(); 273 mmsApp.getRecentSuggestions().saveRecentQuery( 274 searchString, 275 getString(R.string.search_history, 276 cursorCount, searchString)); 277 } 278 } 279 }; 280 281 // don't pass a projection since the search uri ignores it 282 Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter("pattern", searchString).build(); 283 284 // kick off a query for the threads which match the search string 285 mQueryHandler.startQuery(0, null, uri, null, null, null, null); 286 287 } 288} 289