com.aboveware.sms.ui.MessageSearchResultActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.aboveware.sms.ui.MessageSearchResultActivity.java

Source

/**
 * 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);
    }
}