blob: 0181a6664c160f99a9bb0177ac861dfbdf1c196e [file] [log] [blame]
/**
* 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.MmsApp;
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.ListView;
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";
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);
setContentView(R.layout.search_activity);
String searchStringParameter = getIntent().getStringExtra(SearchManager.QUERY).trim();
final String searchString =
searchStringParameter != null ? searchStringParameter.trim() : searchStringParameter;
ContentResolver cr = getContentResolver();
final ListView listView = getListView();
listView.setSelector(android.R.drawable.list_selector_background);
listView.setItemsCanFocus(true);
listView.setFocusable(true);
listView.setClickable(true);
// I considered something like "searching..." but typically it will
// flash on the screen briefly which I found to be more distracting
// than beneficial.
// This gets updated when the query completes.
setTitle("");
// When the query completes cons up a new adapter and set our list adapter to 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");
int cursorCount = c.getCount();
setTitle(getResources().getQuantityString(
R.plurals.search_results_title,
cursorCount,
cursorCount,
searchString));
setListAdapter(new CursorAdapter(SearchActivity.this, c) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
final TextView title = (TextView)(view.findViewById(R.id.title));
final TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle));
String address = cursor.getString(addressPos);
Contact contact = Contact.get(address, false);
contact.addListener(new Contact.UpdateListener() {
public void onUpdate(final Contact updated) {
runOnUiThread(new Runnable() {
public void run() {
title.setText(updated.getNameAndNumber());
}
});
}
});
String titleString = contact.getNameAndNumber();
title.setText(titleString);
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, parent, false);
return v;
}
});
// ListView seems to want to reject the setFocusable until such time
// as the list is not empty. Set it here and request focus. Without
// this the arrow keys (and trackball) fail to move the selection.
listView.setFocusable(true);
listView.setFocusableInTouchMode(true);
listView.requestFocus();
// Remember the query if there are actual results
if (cursorCount > 0) {
MmsApp mmsApp = (MmsApp)getApplication();
mmsApp.getRecentSuggestions().saveRecentQuery(
searchString,
getString(R.string.search_history,
cursorCount, searchString));
}
}
};
// 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, null, null, null, null);
}
}