Java tutorial
/** * 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.aboveware.sms.ui; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.ListActionBarActivity; import android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Typeface; import android.os.Bundle; import android.provider.BaseColumns; import android.provider.Telephony.TextBasedSmsColumns; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.NavUtils; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.support.v7.app.ActionBar; import android.text.SpannableString; import android.text.TextPaint; import android.text.style.StyleSpan; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; import com.aboveware.sms.R; import com.aboveware.sms.conversations.ConversationProvider; import com.aboveware.sms.conversations.ConversationsDatabase; import com.aboveware.sms.conversations.PadConversations; /*** * 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 MessageSearchResultActivity extends ListActionBarActivity implements LoaderCallbacks<Cursor> { public static final String MESSAGE_SEARCH = "message.search"; public class SuggestionListAdapter extends CursorAdapter { public SuggestionListAdapter(Context context, Cursor cursor) { super(context, cursor, 0); LayoutInflater.from(context); } @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)); final long threadId = cursor.getLong(ConversationsDatabase.messageThreadIdIndex); String from = PadConversations.getConversation(threadId).getPadRecipients().formatNames(", "); title.setText(from); snippet.setText(cursor.getString(ConversationsDatabase.messageBodyIndex), 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 rowid = cursor.getLong(ConversationsDatabase.messageIdIndex); view.setOnClickListener(new View.OnClickListener() { @TargetApi(19) @Override public void onClick(View v) { final Intent onClickIntent = new Intent(MessageSearchResultActivity.this, ConversationListActivity.class); onClickIntent.putExtra(TextBasedSmsColumns.THREAD_ID, threadId); onClickIntent.putExtra(MESSAGE_SEARCH, searchString); onClickIntent.putExtra(BaseColumns._ID, rowid); onClickIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(onClickIntent); finish(); } }); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); return inflater.inflate(R.layout.search_item, parent, false); } } /* * 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 StyleSpan highLight = new StyleSpan(Typeface.BOLD); private static String sEllipsis = "\u2026"; private static String snippetString = null; private String mFullText; private Pattern mPattern; private String mTargetString; public TextViewSnippet(Context context) { super(context); } public TextViewSnippet(Context context, AttributeSet attrs) { super(context, attrs); } 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. */ @SuppressLint("DrawAllocation") @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { String fullTextLower = mFullText.toLowerCase(Locale.getDefault()); String targetStringLower = mTargetString.toLowerCase(Locale.getDefault()); int startPos = 0; int searchStringLength = targetStringLower.length(); int bodyLength = fullTextLower.length(); Matcher m = mPattern.matcher(mFullText); if (m.find(0)) { startPos = m.start(); } TextPaint tp = getPaint(); float searchStringWidth = tp.measureText(mTargetString); float textFieldWidth = getWidth(); float ellipsisWidth = tp.measureText(sEllipsis); textFieldWidth -= (2F * ellipsisWidth); // assume we'll need one on both // ends snippetString = null; if (searchStringWidth > textFieldWidth) { snippetString = mFullText.substring(startPos, startPos + searchStringLength); } else { int offset = -1; int start = -1; int end = -1; 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); } } SpannableString spannable = new SpannableString(snippetString); int start = 0; m = mPattern.matcher(snippetString); while (m.find(start)) { spannable.setSpan(highLight, m.start(), m.end(), 0); start = m.end(); } setText(spannable); // do this after the call to setText() above super.onLayout(changed, left, top, right, bottom); } public void setText(String fullText, String target) { // Use a regular expression to locate the target string within the full text. The target string must be // found as a word start so we use \b which matches word boundaries. String patternString = "\\b" + Pattern.quote(target); mPattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE); mFullText = fullText; mTargetString = target; requestLayout(); } } private String searchString = ""; private SuggestionListAdapter suggestionAdapter; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); String searchStringParameter = getIntent().getStringExtra(SearchManager.QUERY); searchString = searchStringParameter != null ? searchStringParameter.trim() : ""; setContentView(R.layout.search_activity); final ListView listView = getListView(); listView.setItemsCanFocus(true); listView.setFocusable(true); listView.setClickable(true); // Create an empty adapter we will use to display the loaded data. suggestionAdapter = new SuggestionListAdapter(this, null); setListAdapter(suggestionAdapter); getSupportLoaderManager().initLoader(0, null, this); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); } @Override public Loader<Cursor> onCreateLoader(int arg0, Bundle arg) { return ConversationProvider.suggestionLoader(this, searchString); } @Override public void onLoaderReset(Loader<Cursor> loader) { suggestionAdapter.swapCursor(null); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if (cursor == null) { setTitle(getResources().getQuantityString(R.plurals.search_results_title, 0, 0, searchString)); return; } int cursorCount = cursor.getCount(); setTitle(getResources().getQuantityString(R.plurals.search_results_title, cursorCount, cursorCount, searchString)); suggestionAdapter.swapCursor(cursor); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: // This ID represents the Home or Up button. In the case of this // activity, the Up button is shown. Use NavUtils to allow users // to navigate up one level in the application structure. For // more details, see the Navigation pattern on Android Design: // // http://developer.android.com/design/patterns/navigation.html#up-vs-back // NavUtils.navigateUpTo(this, new Intent(this, ConversationListActivity.class)); return true; } return super.onOptionsItemSelected(item); } }