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