SearchActivity.java revision f1ab045ff991e1ec3e4161d213627b8629971c0e
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.MmsApp;
20import com.android.mms.R;
21import android.app.ListActivity;
22import android.app.SearchManager;
23import android.content.AsyncQueryHandler;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.database.Cursor;
28import android.graphics.Color;
29import android.graphics.Typeface;
30import android.net.Uri;
31import android.os.Bundle;
32import android.provider.Telephony;
33import android.text.SpannableString;
34import android.text.TextPaint;
35import android.text.style.ForegroundColorSpan;
36import android.text.style.StyleSpan;
37import android.util.AttributeSet;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.CursorAdapter;
42import android.widget.ListView;
43import android.widget.TextView;
44
45import com.android.mms.data.Contact;
46import com.android.mms.ui.ComposeMessageActivity;
47
48/***
49 * Presents a List of search results.  Each item in the list represents a thread which
50 * matches.  The item contains the contact (or phone number) as the "title" and a
51 * snippet of what matches, below.  The snippet is taken from the most recent part of
52 * the conversation that has a match.  Each match within the visible portion of the
53 * snippet is highlighted.
54 */
55
56public class SearchActivity extends ListActivity
57{
58    AsyncQueryHandler mQueryHandler;
59
60    /*
61     * Subclass of TextView which displays a snippet of text which matches the full text and
62     * highlights the matches within the snippet.
63     */
64    public static class TextViewSnippet extends TextView {
65        private static String sEllipsis = "\u2026";
66
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        setContentView(R.layout.search_activity);
175
176        String searchStringParameter = getIntent().getStringExtra(SearchManager.QUERY).trim();
177        final String searchString =
178        	searchStringParameter != null ? searchStringParameter.trim() : searchStringParameter;
179        ContentResolver cr = getContentResolver();
180
181        final ListView listView = getListView();
182        listView.setSelector(android.R.drawable.list_selector_background);
183        listView.setItemsCanFocus(true);
184        listView.setFocusable(true);
185        listView.setClickable(true);
186
187        // I considered something like "searching..." but typically it will
188        // flash on the screen briefly which I found to be more distracting
189        // than beneficial.
190        // This gets updated when the query completes.
191        setTitle("");
192
193        // When the query completes cons up a new adapter and set our list adapter to that.
194        mQueryHandler = new AsyncQueryHandler(cr) {
195            protected void onQueryComplete(int token, Object cookie, Cursor c) {
196                final int threadIdPos = c.getColumnIndex("thread_id");
197                final int addressPos  = c.getColumnIndex("address");
198                final int bodyPos     = c.getColumnIndex("body");
199                final int rowidPos    = c.getColumnIndex("_id");
200
201                int cursorCount = c.getCount();
202                setTitle(getResources().getQuantityString(
203                        R.plurals.search_results_title,
204                        cursorCount,
205                        cursorCount,
206                        searchString));
207
208                // Note that we're telling the CursorAdapter not to do auto-requeries. If we
209                // want to dynamically respond to changes in the search results,
210                // we'll have have to add a setOnDataSetChangedListener().
211                setListAdapter(new CursorAdapter(SearchActivity.this,
212                        c, false /* no auto-requery */) {
213                    @Override
214                    public void bindView(View view, Context context, Cursor cursor) {
215                        final TextView title = (TextView)(view.findViewById(R.id.title));
216                        final TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle));
217
218                        String address = cursor.getString(addressPos);
219                        Contact contact = Contact.get(address, false);
220
221                        contact.addListener(new Contact.UpdateListener() {
222                            public void onUpdate(final Contact updated) {
223                                runOnUiThread(new Runnable() {
224                                    public void run() {
225                                        title.setText(updated.getNameAndNumber());
226                                    }
227                                });
228                            }
229                        });
230
231                        String titleString = contact.getNameAndNumber();
232                        title.setText(titleString);
233
234                        snippet.setText(cursor.getString(bodyPos), searchString);
235
236                        // if the user touches the item then launch the compose message
237                        // activity with some extra parameters to highlight the search
238                        // results and scroll to the latest part of the conversation
239                        // that has a match.
240                        final long threadId = cursor.getLong(threadIdPos);
241                        final long rowid = cursor.getLong(rowidPos);
242
243                        view.setOnClickListener(new View.OnClickListener() {
244                            public void onClick(View v) {
245                                final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class);
246                                onClickIntent.putExtra("thread_id", threadId);
247                                onClickIntent.putExtra("highlight", searchString);
248                                onClickIntent.putExtra("select_id", rowid);
249                                startActivity(onClickIntent);
250                            }
251                        });
252                    }
253
254                    @Override
255                    public View newView(Context context, Cursor cursor, ViewGroup parent) {
256                        LayoutInflater inflater = LayoutInflater.from(context);
257                        View v = inflater.inflate(R.layout.search_item, parent, false);
258                        return v;
259                    }
260
261                });
262
263                // ListView seems to want to reject the setFocusable until such time
264                // as the list is not empty.  Set it here and request focus.  Without
265                // this the arrow keys (and trackball) fail to move the selection.
266                listView.setFocusable(true);
267                listView.setFocusableInTouchMode(true);
268                listView.requestFocus();
269
270                // Remember the query if there are actual results
271                if (cursorCount > 0) {
272                    MmsApp mmsApp = (MmsApp)getApplication();
273                    mmsApp.getRecentSuggestions().saveRecentQuery(
274                            searchString,
275                            getString(R.string.search_history,
276                                    cursorCount, searchString));
277                }
278            }
279        };
280
281        // don't pass a projection since the search uri ignores it
282        Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon().appendQueryParameter("pattern", searchString).build();
283
284        // kick off a query for the threads which match the search string
285        mQueryHandler.startQuery(0, null, uri, null, null, null, null);
286
287    }
288}
289