SearchActivity.java revision 8447aa1d84a469e8938b53a60b0763633b08ae98
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 setListAdapter(new CursorAdapter(SearchActivity.this, c) { 209 @Override 210 public void bindView(View view, Context context, Cursor cursor) { 211 final TextView title = (TextView)(view.findViewById(R.id.title)); 212 final TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle)); 213 214 String address = cursor.getString(addressPos); 215 Contact contact = Contact.get(address, false); 216 217 contact.addListener(new Contact.UpdateListener() { 218 public void onUpdate(final Contact updated) { 219 runOnUiThread(new Runnable() { 220 public void run() { 221 title.setText(updated.getNameAndNumber()); 222 } 223 }); 224 } 225 }); 226 227 String titleString = contact.getNameAndNumber(); 228 title.setText(titleString); 229 230 snippet.setText(cursor.getString(bodyPos), searchString); 231 232 // if the user touches the item then launch the compose message 233 // activity with some extra parameters to highlight the search 234 // results and scroll to the latest part of the conversation 235 // that has a match. 236 final long threadId = cursor.getLong(threadIdPos); 237 final long rowid = cursor.getLong(rowidPos); 238 239 view.setOnClickListener(new View.OnClickListener() { 240 public void onClick(View v) { 241 final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class); 242 onClickIntent.putExtra("thread_id", threadId); 243 onClickIntent.putExtra("highlight", searchString); 244 onClickIntent.putExtra("select_id", rowid); 245 startActivity(onClickIntent); 246 } 247 }); 248 } 249 250 @Override 251 public View newView(Context context, Cursor cursor, ViewGroup parent) { 252 LayoutInflater inflater = LayoutInflater.from(context); 253 View v = inflater.inflate(R.layout.search_item, parent, false); 254 return v; 255 } 256 257 }); 258 259 // ListView seems to want to reject the setFocusable until such time 260 // as the list is not empty. Set it here and request focus. Without 261 // this the arrow keys (and trackball) fail to move the selection. 262 listView.setFocusable(true); 263 listView.setFocusableInTouchMode(true); 264 listView.requestFocus(); 265 266 // Remember the query if there are actual results 267 if (cursorCount > 0) { 268 MmsApp mmsApp = (MmsApp)getApplication(); 269 mmsApp.getRecentSuggestions().saveRecentQuery( 270 searchString, 271 getString(R.string.search_history, 272 cursorCount, searchString)); 273 } 274 } 275 }; 276 277 // don't pass a projection since the search uri ignores it 278 Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter("pattern", searchString).build(); 279 280 // kick off a query for the threads which match the search string 281 mQueryHandler.startQuery(0, null, uri, null, null, null, null); 282 283 } 284} 285