Home | History | Annotate | Download | only in ui
      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 
     82         public TextViewSnippet(Context context, AttributeSet attrs) {
     83             super(context, attrs);
     84         }
     85 
     86         public TextViewSnippet(Context context) {
     87             super(context);
     88         }
     89 
     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
     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 
    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
    200     public void onStop() {
    201         super.onStop();
    202         Contact.removeListener(mContactListener);
    203     }
    204 
    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
    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
    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