SearchActivity.java revision 331864544ec51ba6807fc5471cc6d537b7fef198
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.R; 20import android.app.ListActivity; 21import android.app.SearchManager; 22import android.content.AsyncQueryHandler; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.Intent; 26import android.database.Cursor; 27import android.graphics.Color; 28import android.graphics.Typeface; 29import android.net.Uri; 30import android.os.Bundle; 31import android.provider.Telephony; 32import android.text.SpannableString; 33import android.text.TextPaint; 34import android.text.style.ForegroundColorSpan; 35import android.text.style.StyleSpan; 36import android.util.AttributeSet; 37import android.view.LayoutInflater; 38import android.view.View; 39import android.view.ViewGroup; 40import android.widget.CursorAdapter; 41import android.widget.TextView; 42 43import com.android.mms.data.Contact; 44import com.android.mms.ui.ComposeMessageActivity; 45 46/*** 47 * Presents a List of search results. Each item in the list represents a thread which 48 * matches. The item contains the contact (or phone number) as the "title" and a 49 * snippet of what matches, below. The snippet is taken from the most recent part of 50 * the conversation that has a match. Each match within the visible portion of the 51 * snippet is highlighted. 52 */ 53 54public class SearchActivity extends ListActivity 55{ 56 AsyncQueryHandler mQueryHandler; 57 58 /* 59 * Subclass of TextView which displays a snippet of text which matches the full text and 60 * highlights the matches within the snippet. 61 */ 62 public static class TextViewSnippet extends TextView { 63 private static String sEllipsis = "\u2026"; 64 65 // todo move to resource file 66 private static int sHighlightColor = Color.MAGENTA; 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 175 final String searchString = getIntent().getStringExtra(SearchManager.QUERY); 176 ContentResolver cr = getContentResolver(); 177 178 // When the query completes cons up a new adapter and set our list adapter tot that. 179 mQueryHandler = new AsyncQueryHandler(cr) { 180 protected void onQueryComplete(int token, Object cookie, Cursor c) { 181 final int threadIdPos = c.getColumnIndex("thread_id"); 182 final int addressPos = c.getColumnIndex("address"); 183 final int bodyPos = c.getColumnIndex("body"); 184 final int rowidPos = c.getColumnIndex("_id"); 185 186 setListAdapter(new CursorAdapter(SearchActivity.this, c) { 187 @Override 188 public void bindView(View view, Context context, Cursor cursor) { 189 TextView title = (TextView)(view.findViewById(R.id.title)); 190 TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle)); 191 192 String address = cursor.getString(addressPos); 193 Contact contact = Contact.get(address, true); 194 195 title.setText(contact.getNameAndNumber()); 196 197 snippet.setText(cursor.getString(bodyPos), searchString); 198 199 // if the user touches the item then launch the compose message 200 // activity with some extra parameters to highlight the search 201 // results and scroll to the latest part of the conversation 202 // that has a match. 203 final long threadId = cursor.getLong(threadIdPos); 204 final long rowid = cursor.getLong(rowidPos); 205 206 view.setOnClickListener(new View.OnClickListener() { 207 public void onClick(View v) { 208 final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class); 209 onClickIntent.putExtra("thread_id", threadId); 210 onClickIntent.putExtra("highlight", searchString); 211 onClickIntent.putExtra("select_id", rowid); 212 startActivity(onClickIntent); 213 } 214 }); 215 } 216 217 @Override 218 public View newView(Context context, Cursor cursor, ViewGroup parent) { 219 LayoutInflater inflater = LayoutInflater.from(context); 220 View v = inflater.inflate(R.layout.search_item, null); 221 return v; 222 } 223 224 }); 225 } 226 }; 227 228 // don't pass a projection since the search uri ignores it 229 Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter("pattern", searchString).build(); 230 231 // kick off a query for the threads which match the search string 232 mQueryHandler.startQuery(0, null, uri, new String[]{ "body" }, null, null, null); 233 234 } 235} 236