/** * Copyright (c) 2009, Google Inc. * * 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 com.android.mms.ui; import com.android.mms.R; import android.app.ListActivity; import android.app.SearchManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.provider.Telephony; import android.text.SpannableString; import android.text.TextPaint; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.TextView; import com.android.mms.data.Contact; import com.android.mms.ui.ComposeMessageActivity; /*** * Presents a List of search results. Each item in the list represents a thread which * matches. The item contains the contact (or phone number) as the "title" and a * snippet of what matches, below. The snippet is taken from the most recent part of * the conversation that has a match. Each match within the visible portion of the * snippet is highlighted. */ public class SearchActivity extends ListActivity { AsyncQueryHandler mQueryHandler; /* * Subclass of TextView which displays a snippet of text which matches the full text and * highlights the matches within the snippet. */ public static class TextViewSnippet extends TextView { private static String sEllipsis = "\u2026"; // todo move to resource file private static int sHighlightColor = Color.MAGENTA; private static int sTypefaceHighlight = Typeface.BOLD; private String mFullText; private String mTargetString; public TextViewSnippet(Context context, AttributeSet attrs) { super(context, attrs); } public TextViewSnippet(Context context) { super(context); } public TextViewSnippet(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * We have to know our width before we can compute the snippet string. Do that * here and then defer to super for whatever work is normally done. */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { String fullTextLower = mFullText.toLowerCase(); String targetStringLower = mTargetString.toLowerCase(); int startPos = fullTextLower.indexOf(targetStringLower); int searchStringLength = targetStringLower.length(); int bodyLength = fullTextLower.length(); TextPaint tp = getPaint(); float searchStringWidth = tp.measureText(mTargetString); float textFieldWidth = getWidth(); String snippetString = null; if (searchStringWidth > textFieldWidth) { snippetString = mFullText.substring(startPos, startPos + searchStringLength); } else { float ellipsisWidth = tp.measureText(sEllipsis); textFieldWidth -= (2F * ellipsisWidth); // assume we'll need one on both ends int offset = -1; int start = -1; int end = -1; /* TODO: this code could be made more efficient by only measuring the additional * characters as we widen the string rather than measuring the whole new * string each time. */ while (true) { offset += 1; int newstart = Math.max(0, startPos - offset); int newend = Math.min(bodyLength, startPos + searchStringLength + offset); if (newstart == start && newend == end) { // if we couldn't expand out any further then we're done break; } start = newstart; end = newend; // pull the candidate string out of the full text rather than body // because body has been toLower()'ed String candidate = mFullText.substring(start, end); if (tp.measureText(candidate) > textFieldWidth) { // if the newly computed width would exceed our bounds then we're done // do not use this "candidate" break; } snippetString = String.format( "%s%s%s", start == 0 ? "" : sEllipsis, candidate, end == bodyLength ? "" : sEllipsis); } } String snippetStringLower = snippetString.toLowerCase(); SpannableString spannable = new SpannableString(snippetString); int start = 0; while (true) { int index = snippetStringLower.indexOf(targetStringLower, start); if (index == -1) { break; } spannable.setSpan(new ForegroundColorSpan(sHighlightColor), index, index+searchStringLength, 0); spannable.setSpan(new StyleSpan(sTypefaceHighlight), index, index+searchStringLength, 0); start = index + searchStringLength; } setText(spannable); // do this after the call to setText() above super.onLayout(changed, left, top, right, bottom); } public void setText(String fullText, String target) { mFullText = fullText; mTargetString = target; requestLayout(); } } public void onCreate(Bundle icicle) { super.onCreate(icicle); final String searchString = getIntent().getStringExtra(SearchManager.QUERY); ContentResolver cr = getContentResolver(); // When the query completes cons up a new adapter and set our list adapter tot that. mQueryHandler = new AsyncQueryHandler(cr) { protected void onQueryComplete(int token, Object cookie, Cursor c) { final int threadIdPos = c.getColumnIndex("thread_id"); final int addressPos = c.getColumnIndex("address"); final int bodyPos = c.getColumnIndex("body"); final int rowidPos = c.getColumnIndex("_id"); setListAdapter(new CursorAdapter(SearchActivity.this, c) { @Override public void bindView(View view, Context context, Cursor cursor) { TextView title = (TextView)(view.findViewById(R.id.title)); TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle)); String address = cursor.getString(addressPos); Contact contact = Contact.get(address, true); title.setText(contact.getNameAndNumber()); snippet.setText(cursor.getString(bodyPos), searchString); // if the user touches the item then launch the compose message // activity with some extra parameters to highlight the search // results and scroll to the latest part of the conversation // that has a match. final long threadId = cursor.getLong(threadIdPos); final long rowid = cursor.getLong(rowidPos); view.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class); onClickIntent.putExtra("thread_id", threadId); onClickIntent.putExtra("highlight", searchString); onClickIntent.putExtra("select_id", rowid); startActivity(onClickIntent); } }); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); View v = inflater.inflate(R.layout.search_item, null); return v; } }); } }; // don't pass a projection since the search uri ignores it Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter("pattern", searchString).build(); // kick off a query for the threads which match the search string mQueryHandler.startQuery(0, null, uri, new String[]{ "body" }, null, null, null); } }