• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.mms.ui;
18 
19 import java.util.HashMap;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22 
23 import android.app.ActionBar;
24 import android.app.ListActivity;
25 import android.app.SearchManager;
26 import android.content.AsyncQueryHandler;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.database.Cursor;
31 import android.graphics.Typeface;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.provider.SearchRecentSuggestions;
35 import android.provider.Telephony;
36 import android.text.SpannableString;
37 import android.text.TextPaint;
38 import android.text.style.StyleSpan;
39 import android.util.AttributeSet;
40 import android.view.LayoutInflater;
41 import android.view.MenuItem;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.CursorAdapter;
45 import android.widget.ListView;
46 import android.widget.TextView;
47 
48 import com.android.mms.MmsApp;
49 import com.android.mms.R;
50 import com.android.mms.data.Contact;
51 
52 /***
53  * Presents a List of search results.  Each item in the list represents a thread which
54  * matches.  The item contains the contact (or phone number) as the "title" and a
55  * snippet of what matches, below.  The snippet is taken from the most recent part of
56  * the conversation that has a match.  Each match within the visible portion of the
57  * snippet is highlighted.
58  */
59 
60 public class SearchActivity extends ListActivity
61 {
62     private AsyncQueryHandler mQueryHandler;
63 
64     // Track which TextView's show which Contact objects so that we can update
65     // appropriately when the Contact gets fully loaded.
66     private HashMap<Contact, TextView> mContactMap = new HashMap<Contact, TextView>();
67 
68 
69     /*
70      * Subclass of TextView which displays a snippet of text which matches the full text and
71      * highlights the matches within the snippet.
72      */
73     public static class TextViewSnippet extends TextView {
74         private static String sEllipsis = "\u2026";
75 
76         private static int sTypefaceHighlight = Typeface.BOLD;
77 
78         private String mFullText;
79         private String mTargetString;
80         private Pattern mPattern;
81 
TextViewSnippet(Context context, AttributeSet attrs)82         public TextViewSnippet(Context context, AttributeSet attrs) {
83             super(context, attrs);
84         }
85 
TextViewSnippet(Context context)86         public TextViewSnippet(Context context) {
87             super(context);
88         }
89 
TextViewSnippet(Context context, AttributeSet attrs, int defStyle)90         public TextViewSnippet(Context context, AttributeSet attrs, int defStyle) {
91             super(context, attrs, defStyle);
92         }
93 
94         /**
95          * We have to know our width before we can compute the snippet string.  Do that
96          * here and then defer to super for whatever work is normally done.
97          */
98         @Override
onLayout(boolean changed, int left, int top, int right, int bottom)99         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
100             String fullTextLower = mFullText.toLowerCase();
101             String targetStringLower = mTargetString.toLowerCase();
102 
103             int startPos = 0;
104             int searchStringLength = targetStringLower.length();
105             int bodyLength = fullTextLower.length();
106 
107             Matcher m = mPattern.matcher(mFullText);
108             if (m.find(0)) {
109                 startPos = m.start();
110             }
111 
112             TextPaint tp = getPaint();
113 
114             float searchStringWidth = tp.measureText(mTargetString);
115             float textFieldWidth = getWidth();
116 
117             float ellipsisWidth = tp.measureText(sEllipsis);
118             textFieldWidth -= (2F * ellipsisWidth); // assume we'll need one on both ends
119 
120             String snippetString = null;
121             if (searchStringWidth > textFieldWidth) {
122                 snippetString = mFullText.substring(startPos, startPos + searchStringLength);
123             } else {
124 
125                 int offset = -1;
126                 int start = -1;
127                 int end = -1;
128                 /* TODO: this code could be made more efficient by only measuring the additional
129                  * characters as we widen the string rather than measuring the whole new
130                  * string each time.
131                  */
132                 while (true) {
133                     offset += 1;
134 
135                     int newstart = Math.max(0, startPos - offset);
136                     int newend = Math.min(bodyLength, startPos + searchStringLength + offset);
137 
138                     if (newstart == start && newend == end) {
139                         // if we couldn't expand out any further then we're done
140                         break;
141                     }
142                     start = newstart;
143                     end = newend;
144 
145                     // pull the candidate string out of the full text rather than body
146                     // because body has been toLower()'ed
147                     String candidate = mFullText.substring(start, end);
148                     if (tp.measureText(candidate) > textFieldWidth) {
149                         // if the newly computed width would exceed our bounds then we're done
150                         // do not use this "candidate"
151                         break;
152                     }
153 
154                     snippetString = String.format(
155                             "%s%s%s",
156                             start == 0 ? "" : sEllipsis,
157                             candidate,
158                             end == bodyLength ? "" : sEllipsis);
159                 }
160             }
161 
162             SpannableString spannable = new SpannableString(snippetString);
163             int start = 0;
164 
165             m = mPattern.matcher(snippetString);
166             while (m.find(start)) {
167                 spannable.setSpan(new StyleSpan(sTypefaceHighlight), m.start(), m.end(), 0);
168                 start = m.end();
169             }
170             setText(spannable);
171 
172             // do this after the call to setText() above
173             super.onLayout(changed, left, top, right, bottom);
174         }
175 
setText(String fullText, String target)176         public void setText(String fullText, String target) {
177             // Use a regular expression to locate the target string
178             // within the full text.  The target string must be
179             // found as a word start so we use \b which matches
180             // word boundaries.
181             String patternString = "\\b" + Pattern.quote(target);
182             mPattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
183 
184             mFullText = fullText;
185             mTargetString = target;
186             requestLayout();
187         }
188     }
189 
190     Contact.UpdateListener mContactListener = new Contact.UpdateListener() {
191         public void onUpdate(Contact updated) {
192             TextView tv = mContactMap.get(updated);
193             if (tv != null) {
194                 tv.setText(updated.getNameAndNumber());
195             }
196         }
197     };
198 
199     @Override
onStop()200     public void onStop() {
201         super.onStop();
202         Contact.removeListener(mContactListener);
203     }
204 
getThreadId(long sourceId, long which)205     private long getThreadId(long sourceId, long which) {
206         Uri.Builder b = Uri.parse("content://mms-sms/messageIdToThread").buildUpon();
207         b = b.appendQueryParameter("row_id", String.valueOf(sourceId));
208         b = b.appendQueryParameter("table_to_use", String.valueOf(which));
209         String s = b.build().toString();
210 
211         Cursor c = getContentResolver().query(
212                 Uri.parse(s),
213                 null,
214                 null,
215                 null,
216                 null);
217         if (c != null) {
218             try {
219                 if (c.moveToFirst()) {
220                     return c.getLong(c.getColumnIndex("thread_id"));
221                 }
222             } finally {
223                 c.close();
224             }
225         }
226         return -1;
227     }
228 
229     @Override
onCreate(Bundle icicle)230     public void onCreate(Bundle icicle) {
231         super.onCreate(icicle);
232 
233         String searchStringParameter = getIntent().getStringExtra(SearchManager.QUERY);
234         if (searchStringParameter == null) {
235             searchStringParameter = getIntent().getStringExtra("intent_extra_data_key" /*SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA*/);
236         }
237         final String searchString =
238             searchStringParameter != null ? searchStringParameter.trim() : searchStringParameter;
239 
240         // If we're being launched with a source_id then just go to that particular thread.
241         // Work around the fact that suggestions can only launch the search activity, not some
242         // arbitrary activity (such as ComposeMessageActivity).
243         final Uri u = getIntent().getData();
244         if (u != null && u.getQueryParameter("source_id") != null) {
245             Thread t = new Thread(new Runnable() {
246                 public void run() {
247                     try {
248                         long sourceId = Long.parseLong(u.getQueryParameter("source_id"));
249                         long whichTable = Long.parseLong(u.getQueryParameter("which_table"));
250                         long threadId = getThreadId(sourceId, whichTable);
251 
252                         final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class);
253                         onClickIntent.putExtra("highlight", searchString);
254                         onClickIntent.putExtra("select_id", sourceId);
255                         onClickIntent.putExtra("thread_id", threadId);
256                         startActivity(onClickIntent);
257                         finish();
258                         return;
259                     } catch (NumberFormatException ex) {
260                         // ok, we do not have a thread id so continue
261                     }
262                 }
263             }, "Search thread");
264             t.start();
265             return;
266         }
267 
268         setContentView(R.layout.search_activity);
269         ContentResolver cr = getContentResolver();
270 
271         searchStringParameter = searchStringParameter.trim();
272         final ListView listView = getListView();
273         listView.setItemsCanFocus(true);
274         listView.setFocusable(true);
275         listView.setClickable(true);
276 
277         // I considered something like "searching..." but typically it will
278         // flash on the screen briefly which I found to be more distracting
279         // than beneficial.
280         // This gets updated when the query completes.
281         setTitle("");
282 
283         Contact.addListener(mContactListener);
284 
285         // When the query completes cons up a new adapter and set our list adapter to that.
286         mQueryHandler = new AsyncQueryHandler(cr) {
287             protected void onQueryComplete(int token, Object cookie, Cursor c) {
288                 if (c == null) {
289                     setTitle(getResources().getQuantityString(
290                         R.plurals.search_results_title,
291                         0,
292                         0,
293                         searchString));
294                     return;
295                 }
296                 final int threadIdPos = c.getColumnIndex("thread_id");
297                 final int addressPos  = c.getColumnIndex("address");
298                 final int bodyPos     = c.getColumnIndex("body");
299                 final int rowidPos    = c.getColumnIndex("_id");
300 
301                 int cursorCount = c.getCount();
302                 setTitle(getResources().getQuantityString(
303                         R.plurals.search_results_title,
304                         cursorCount,
305                         cursorCount,
306                         searchString));
307 
308                 // Note that we're telling the CursorAdapter not to do auto-requeries. If we
309                 // want to dynamically respond to changes in the search results,
310                 // we'll have have to add a setOnDataSetChangedListener().
311                 setListAdapter(new CursorAdapter(SearchActivity.this,
312                         c, false /* no auto-requery */) {
313                     @Override
314                     public void bindView(View view, Context context, Cursor cursor) {
315                         final TextView title = (TextView)(view.findViewById(R.id.title));
316                         final TextViewSnippet snippet = (TextViewSnippet)(view.findViewById(R.id.subtitle));
317 
318                         String address = cursor.getString(addressPos);
319                         Contact contact = address != null ? Contact.get(address, false) : null;
320 
321                         String titleString = contact != null ? contact.getNameAndNumber() : "";
322                         title.setText(titleString);
323 
324                         snippet.setText(cursor.getString(bodyPos), searchString);
325 
326                         // if the user touches the item then launch the compose message
327                         // activity with some extra parameters to highlight the search
328                         // results and scroll to the latest part of the conversation
329                         // that has a match.
330                         final long threadId = cursor.getLong(threadIdPos);
331                         final long rowid = cursor.getLong(rowidPos);
332 
333                         view.setOnClickListener(new View.OnClickListener() {
334                             public void onClick(View v) {
335                                 final Intent onClickIntent = new Intent(SearchActivity.this, ComposeMessageActivity.class);
336                                 onClickIntent.putExtra("thread_id", threadId);
337                                 onClickIntent.putExtra("highlight", searchString);
338                                 onClickIntent.putExtra("select_id", rowid);
339                                 startActivity(onClickIntent);
340                             }
341                         });
342                     }
343 
344                     @Override
345                     public View newView(Context context, Cursor cursor, ViewGroup parent) {
346                         LayoutInflater inflater = LayoutInflater.from(context);
347                         View v = inflater.inflate(R.layout.search_item, parent, false);
348                         return v;
349                     }
350 
351                 });
352 
353                 // ListView seems to want to reject the setFocusable until such time
354                 // as the list is not empty.  Set it here and request focus.  Without
355                 // this the arrow keys (and trackball) fail to move the selection.
356                 listView.setFocusable(true);
357                 listView.setFocusableInTouchMode(true);
358                 listView.requestFocus();
359 
360                 // Remember the query if there are actual results
361                 if (cursorCount > 0) {
362                     SearchRecentSuggestions recent = ((MmsApp)getApplication()).getRecentSuggestions();
363                     if (recent != null) {
364                         recent.saveRecentQuery(
365                                 searchString,
366                                 getString(R.string.search_history,
367                                         cursorCount, searchString));
368                     }
369                 }
370             }
371         };
372 
373         // don't pass a projection since the search uri ignores it
374         Uri uri = Telephony.MmsSms.SEARCH_URI.buildUpon()
375                     .appendQueryParameter("pattern", searchString).build();
376 
377         // kick off a query for the threads which match the search string
378         mQueryHandler.startQuery(0, null, uri, null, null, null, null);
379 
380         ActionBar actionBar = getActionBar();
381         actionBar.setDisplayHomeAsUpEnabled(true);
382     }
383 
384     @Override
onOptionsItemSelected(MenuItem item)385     public boolean onOptionsItemSelected(MenuItem item) {
386         switch (item.getItemId()) {
387             case android.R.id.home:
388                 // The user clicked on the Messaging icon in the action bar. Take them back from
389                 // wherever they came from
390                 finish();
391                 return true;
392         }
393         return false;
394     }
395 }
396