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