]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
go
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 package com.hughes.android.dictionary;
2
3 import java.io.File;
4 import java.io.FileWriter;
5 import java.io.IOException;
6 import java.io.PrintWriter;
7 import java.io.RandomAccessFile;
8 import java.text.SimpleDateFormat;
9 import java.util.Arrays;
10 import java.util.Date;
11 import java.util.concurrent.Executor;
12 import java.util.concurrent.Executors;
13 import java.util.concurrent.atomic.AtomicBoolean;
14
15 import android.app.ListActivity;
16 import android.content.Context;
17 import android.content.Intent;
18 import android.content.SharedPreferences;
19 import android.content.SharedPreferences.Editor;
20 import android.graphics.Typeface;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.preference.PreferenceManager;
24 import android.text.ClipboardManager;
25 import android.text.Editable;
26 import android.text.Spannable;
27 import android.text.TextWatcher;
28 import android.text.style.StyleSpan;
29 import android.util.Log;
30 import android.view.ContextMenu;
31 import android.view.KeyEvent;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ContextMenu.ContextMenuInfo;
37 import android.view.MenuItem.OnMenuItemClickListener;
38 import android.view.View.OnClickListener;
39 import android.view.inputmethod.InputMethodManager;
40 import android.widget.AdapterView;
41 import android.widget.BaseAdapter;
42 import android.widget.Button;
43 import android.widget.EditText;
44 import android.widget.ListView;
45 import android.widget.TableLayout;
46 import android.widget.TableRow;
47 import android.widget.TextView;
48 import android.widget.Toast;
49
50 import com.hughes.android.dictionary.Dictionary.IndexEntry;
51 import com.hughes.android.dictionary.Dictionary.LanguageData;
52 import com.hughes.android.dictionary.Dictionary.Row;
53 import com.ibm.icu.text.Collator;
54
55 public class DictionaryActivity extends ListActivity {
56   
57   // TO DO:
58   // * Easy reverse lookup.
59   // * Download latest dicts.
60   //   * http://ftp.tu-chemnitz.de/pub/Local/urz/ding/de-en-devel/
61   //   * http://www1.dict.cc/translation_file_request.php?l=e
62   // * Compress all the strings everywhere, put compression table in file.
63   // Done:
64   // * Only one way to way for current search to end. (won't do).
65
66   static final String LOG = "QuickDic";
67   static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
68   static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
69
70   // package for test.
71   final Handler uiHandler = new Handler();
72   private final Executor searchExecutor = Executors.newSingleThreadExecutor();
73
74   EditText searchText;
75   Button langButton;
76   int lastSelectedRow = 0;  // TODO: I'm evil.
77
78   private boolean prefsMightHaveChanged = true;
79
80   // Never null.
81   private File wordList;
82   private RandomAccessFile dictRaf = null;
83   private Dictionary dictionary = null;
84   private boolean saveOnlyFirstSubentry = false;
85
86   // Visible for testing.
87   LanguageListAdapter languageList = null;
88   private SearchOperation searchOperation = null;
89   
90   public DictionaryActivity() {
91
92     searchExecutor.execute(new Runnable() {
93       public void run() {
94         final long startMillis = System.currentTimeMillis();
95         for (final String lang : Arrays.asList("EN", "DE")) {
96           Language.lookup(lang).getFindCollator(); 
97           final Collator c = Language.lookup(lang).getSortCollator(); 
98           if (c.compare("pre-print", "preppy") >= 0) {
99             Log.e(LOG, c.getClass() + " is buggy, lookups may not work properly.");
100           }
101         }
102         Log.d(LOG, "Loading collators took:" + (System.currentTimeMillis() - startMillis));
103       }
104     });
105
106   }
107
108   /** Called when the activity is first created. */
109   @Override
110   public void onCreate(Bundle savedInstanceState) {
111     super.onCreate(savedInstanceState);
112     Log.d(LOG, "onCreate:" + this);
113
114     try {
115       initDictionaryAndPrefs();
116     } catch (Exception e) {
117       return;
118     }
119
120     // UI init.
121
122     setContentView(R.layout.dictionary_activity);
123     searchText = (EditText) findViewById(R.id.SearchText);
124     langButton = (Button) findViewById(R.id.LangButton);
125     
126     Log.d(LOG, "adding text changed listener");
127     searchText.addTextChangedListener(new SearchTextWatcher());
128     
129     getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
130       public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
131           long arg3) {
132         setSelectedRow(row);
133       }
134       public void onNothingSelected(AdapterView<?> arg0) {
135       }
136     });
137     
138     getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
139       public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
140           long arg3) {
141         setSelectedRow(row);
142         return false;
143       }
144     });
145     
146     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
147     clearSearchTextButton.setOnClickListener(new OnClickListener() {
148       public void onClick(View v) {
149         onClearSearchTextButton(clearSearchTextButton);
150       }
151     });
152     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
153         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
154         : View.GONE);
155     
156     final Button langButton = (Button) findViewById(R.id.LangButton);
157     langButton.setOnClickListener(new OnClickListener() {
158       public void onClick(View v) {
159         onLanguageButton();
160       }
161     });
162     
163     final Button upButton = (Button) findViewById(R.id.UpButton);
164     upButton.setOnClickListener(new OnClickListener() {
165       public void onClick(View v) {
166         onUpButton();
167       }
168     });
169     final Button downButton = (Button) findViewById(R.id.DownButton);
170     downButton.setOnClickListener(new OnClickListener() {
171       public void onClick(View v) {
172         onDownButton();
173       }
174     });
175
176     // ContextMenu.
177     registerForContextMenu(getListView());
178
179     updateLangButton();
180   }
181   
182   private void initDictionaryAndPrefs() throws Exception {
183     if (!prefsMightHaveChanged) {
184       return;
185     }
186     closeCurrentDictionary();
187     
188     final SharedPreferences prefs = PreferenceManager
189         .getDefaultSharedPreferences(this);
190     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
191         getString(R.string.wordListFileDefault)));
192     Log.d(LOG, "wordList=" + wordList);
193     
194     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
195
196     final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
197         getString(R.string.dictFileDefault)));
198     Log.d(LOG, "dictFile=" + dictFile);
199     
200     try {
201       if (!dictFile.canRead()) {
202         throw new IOException("Unable to read dictionary file.");
203       }
204       
205       dictRaf = new RandomAccessFile(dictFile, "r");
206       final long startMillis = System.currentTimeMillis();
207       dictionary = new Dictionary(dictRaf);
208       Log.d(LOG, "Read dictionary millis: " + (System.currentTimeMillis() - startMillis));
209     } catch (IOException e) {
210       Log.e(LOG, "Couldn't open dictionary.", e);
211       this.startActivity(new Intent(this, NoDictionaryActivity.class));
212       finish();
213       throw new Exception(e);
214     }
215     
216     final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, SimpleEntry.LANG1) == SimpleEntry.LANG1 ? SimpleEntry.LANG1
217         : SimpleEntry.LANG2;
218     
219     languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
220     setListAdapter(languageList);
221     prefsMightHaveChanged = false;
222   }
223
224   @Override
225   public void onResume() {
226     super.onResume();
227     Log.d(LOG, "onResume:" + this);
228
229     try {
230       initDictionaryAndPrefs();
231     } catch (Exception e) {
232       return;
233     }
234     
235     final SharedPreferences prefs = PreferenceManager
236         .getDefaultSharedPreferences(this);
237     final String searchTextString = prefs
238         .getString(PREF_ACTIVE_SEARCH_TEXT, "");
239     searchText.setText(searchTextString);
240     getListView().requestFocus();
241     onSearchTextChange(searchTextString);
242   }
243
244   @Override
245   public void onPause() {
246     super.onPause();
247     Log.d(LOG, "onPause:" + this);
248     final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
249         .edit();
250     prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
251     prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
252     prefs.commit();
253   }
254
255   @Override
256   public void onStop() {
257     super.onStop();
258     Log.d(LOG, "onStop:" + this);
259     if (isFinishing()) {
260       Log.i(LOG, "isFinishing()==true, closing dictionary.");
261       closeCurrentDictionary();
262     }
263   }
264
265   private void closeCurrentDictionary() {
266     Log.i(LOG, "closeCurrentDictionary");
267     if (dictionary == null) {
268       return;
269     }
270     waitForSearchEnd();
271     languageList = null;
272     setListAdapter(null);
273     Log.d(LOG, "setListAdapter finished.");
274     dictionary = null;
275     try {
276       if (dictRaf != null) {
277         dictRaf.close();
278       }
279     } catch (IOException e) {
280       throw new RuntimeException(e);
281     }
282     dictRaf = null;
283   }
284
285   public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
286     final Row row = languageList.languageData.rows.get(getSelectedRow());
287     return languageList.languageData.rowToString(row, onlyFirstSubentry);
288   }
289
290   // ----------------------------------------------------------------
291   // OptionsMenu
292   // ----------------------------------------------------------------
293
294   private MenuItem switchLanguageMenuItem = null;
295
296   @Override
297   public boolean onCreateOptionsMenu(final Menu menu) {
298     switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
299     switchLanguageMenuItem
300         .setOnMenuItemClickListener(new OnMenuItemClickListener() {
301           public boolean onMenuItemClick(final MenuItem menuItem) {
302             onLanguageButton();
303             return false;
304           }
305         });
306
307     final MenuItem preferences = menu.add(getString(R.string.preferences));
308     preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
309       public boolean onMenuItemClick(final MenuItem menuItem) {
310         prefsMightHaveChanged = true;
311         startActivity(new Intent(DictionaryActivity.this,
312             PreferenceActivity.class));
313         return false;
314       }
315     });
316
317     final MenuItem about = menu.add(getString(R.string.about));
318     about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
319       public boolean onMenuItemClick(final MenuItem menuItem) {
320         final Intent intent = new Intent().setClassName(AboutActivity.class
321             .getPackage().getName(), AboutActivity.class.getCanonicalName());
322         final String currentDictInfo;
323         if (dictionary == null) {
324           currentDictInfo = getString(R.string.noDictLoaded);
325         } else {
326           final LanguageData lang0 = dictionary.languageDatas[0];
327           final LanguageData lang1 = dictionary.languageDatas[1];
328           currentDictInfo = getString(R.string.aboutText, dictionary.dictionaryInfo, dictionary.entries.size(), 
329               lang0.language.symbol, lang0.sortedIndex.size(), lang0.rows.size(),
330               lang1.language.symbol, lang1.sortedIndex.size(), lang1.rows.size());
331         }
332         intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
333             .toString());
334         startActivity(intent);
335         return false;
336       }
337     });
338
339     final MenuItem download = menu.add(getString(R.string.downloadDictionary));
340     download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
341       public boolean onMenuItemClick(final MenuItem menuItem) {
342         prefsMightHaveChanged = true;
343         startDownloadDictActivity(DictionaryActivity.this);
344         return false;
345       }
346     });
347
348     return true;
349   }
350
351   @Override
352   public boolean onPrepareOptionsMenu(final Menu menu) {
353     switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
354         dictionary.languageDatas[SimpleEntry
355             .otherLang(languageList.languageData.lang)].language.symbol));
356     return super.onPrepareOptionsMenu(menu);
357   }
358   
359   void updateLangButton() {
360     langButton.setText(languageList.languageData.language.symbol);
361   }
362
363   // ----------------------------------------------------------------
364   // Event handlers.
365   // ----------------------------------------------------------------
366   
367   void onLanguageButton() {
368     waitForSearchEnd();
369     languageList = new LanguageListAdapter(
370         dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
371             : 0]);
372     Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
373     setListAdapter(languageList);
374     updateLangButton();
375     onSearchTextChange(searchText.getText().toString());
376   }
377
378   void onUpButton() {
379     final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
380     Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
381     jumpToRow(languageList, destRowIndex);
382   }
383
384   void onDownButton() {
385     final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
386     Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
387     jumpToRow(languageList, destRowIndex);
388   }
389
390   void onAppendToWordList() {
391     final int row = getSelectedRow();
392     if (row < 0) {
393       return;
394     }
395     final StringBuilder rawText = new StringBuilder();
396     final String word = languageList.languageData.getIndexEntryForRow(row).word;
397     rawText.append(
398         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
399         .append("\t");
400     rawText.append(word).append("\t");
401     rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
402     Log.d(LOG, "Writing : " + rawText);
403     try {
404       wordList.getParentFile().mkdirs();
405       final PrintWriter out = new PrintWriter(
406           new FileWriter(wordList, true));
407       out.println(rawText.toString());
408       out.close();
409     } catch (IOException e) {
410       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
411       Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
412     }
413     return;
414   }
415
416   void onCopy() {
417     final int row = getSelectedRow();
418     if (row < 0) {
419       return;
420     }
421     Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
422     final StringBuilder result = new StringBuilder();
423     result.append(getSelectedRowRawText(false));
424     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
425     clipboardManager.setText(result.toString());
426     Log.d(LOG, "Copied: " + result);
427   }
428
429   @Override
430   public boolean onKeyDown(int keyCode, KeyEvent event) {
431     if (event.getUnicodeChar() != 0) {
432       if (!searchText.hasFocus()) {
433         searchText.setText("" + (char) event.getUnicodeChar());
434         onSearchTextChange(searchText.getText().toString());
435         searchText.requestFocus();
436       }
437       return true;
438     }
439     return super.onKeyDown(keyCode, event);
440   }
441
442   @Override
443   protected void onListItemClick(ListView l, View v, int row, long id) {
444     setSelectedRow(row);
445     openContextMenu(getListView());
446   }
447
448   void onSearchTextChange(final String searchText) {
449     Log.d(LOG, "onSearchTextChange: " + searchText);
450     synchronized (this) {
451       searchOperation = new SearchOperation(languageList, searchText.trim(), searchOperation);
452       searchExecutor.execute(searchOperation);
453     }
454   }
455
456   private void onClearSearchTextButton(final Button clearSearchTextButton) {
457     clearSearchTextButton.requestFocus();
458     searchText.setText("");
459     searchText.requestFocus();
460     Log.d(LOG, "Trying to show soft keyboard.");
461     final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
462     manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);
463   }
464
465   // ----------------------------------------------------------------
466   // ContextMenu
467   // ----------------------------------------------------------------
468
469   @Override
470   public void onCreateContextMenu(ContextMenu menu, View v,
471       ContextMenuInfo menuInfo) {
472     final int row = getSelectedRow();
473     if (row < 0) {
474       return;
475     }
476
477     final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
478     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
479       public boolean onMenuItemClick(MenuItem item) {
480         onAppendToWordList();
481         return false;
482       }
483     });
484
485     final MenuItem copy = menu.add(android.R.string.copy);
486     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
487       public boolean onMenuItemClick(MenuItem item) {
488         onCopy();
489         return false;
490       }
491     });
492
493   }
494
495   private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
496       final int rowIndex) {
497     Log.d(LOG, "jumpToRow: " + rowIndex);
498     if (dictionaryListAdapter != this.languageList) {
499       Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
500       return;
501     }
502     setSelection(rowIndex);
503     setSelectedRow(rowIndex);
504     getListView().setSelected(true);
505   }
506   
507   // TODO: delete me somehow.
508   private int getSelectedRow() {
509     return lastSelectedRow;
510   }
511   private void setSelectedRow(final int row) {
512     lastSelectedRow = row;
513     Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
514     updateSearchText();
515   }
516
517   private void updateSearchText() {
518     Log.d(LOG, "updateSearchText");
519     final int selectedRowIndex = getSelectedRow();
520     if (!searchText.hasFocus()) {
521       if (selectedRowIndex >= 0) {
522         final String word = languageList.languageData
523             .getIndexEntryForRow(selectedRowIndex).word;
524         if (!word.equals(searchText.getText().toString())) {
525           Log.d(LOG, "updateSearchText: setText: " + word);
526           searchText.setText(word);
527         }
528       } else {
529         Log.w(LOG, "updateSearchText: nothing selected.");
530       }
531     }
532   }
533
534   static void startDownloadDictActivity(final Context context) {
535     final Intent intent = new Intent(context, DownloadActivity.class);
536     final SharedPreferences prefs = PreferenceManager
537         .getDefaultSharedPreferences(context);
538     final String dictFetchUrl = prefs.getString(context
539         .getString(R.string.dictFetchUrlKey), context
540         .getString(R.string.dictFetchUrlDefault));
541     final String dictFileName = prefs.getString(context
542         .getString(R.string.dictFileKey), context
543         .getString(R.string.dictFileDefault));
544     intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
545     intent.putExtra(DownloadActivity.DEST, dictFileName);
546     context.startActivity(intent);
547   }
548
549   class LanguageListAdapter extends BaseAdapter {
550
551     // Visible for testing.
552     final LanguageData languageData;
553
554     LanguageListAdapter(final LanguageData languageData) {
555       this.languageData = languageData;
556     }
557
558     public int getCount() {
559       return languageData.rows.size();
560     }
561
562     public Dictionary.Row getItem(int rowIndex) {
563       assert rowIndex < languageData.rows.size();
564       return languageData.rows.get(rowIndex);
565     }
566
567     public long getItemId(int rowIndex) {
568       return rowIndex;
569     }
570
571     public View getView(final int rowIndex, final View convertView,
572         final ViewGroup parent) {
573       final Row row = getItem(rowIndex);
574
575       // Token row.
576       if (row.isToken()) {
577         TextView result = null;
578         if (convertView instanceof TextView) {
579           result = (TextView) convertView;
580         } else {
581           result = new TextView(parent.getContext());
582         }
583         if (row == null) {
584           return result;
585         }
586         result.setText(languageData.rowToString(row, false));
587         result.setTextAppearance(parent.getContext(),
588             android.R.style.TextAppearance_Large);
589         result.setClickable(false);
590         return result;
591       }
592
593       // Entry row(s).
594       final TableLayout result = new TableLayout(parent.getContext());
595
596       final SimpleEntry entry = new SimpleEntry(null, null);//.entries.get(row.getIndex());
597       final int rowCount = entry.getRowCount();
598       for (int r = 0; r < rowCount; ++r) {
599         final TableRow tableRow = new TableRow(result.getContext());
600
601         TextView column1 = new TextView(tableRow.getContext());
602         TextView column2 = new TextView(tableRow.getContext());
603         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
604         layoutParams.weight = 0.5f;
605
606         if (r > 0) {
607           final TextView spacer = new TextView(tableRow.getContext());
608           spacer.setText(r == 0 ? "� " : " � ");
609           tableRow.addView(spacer);
610         }
611         tableRow.addView(column1, layoutParams);
612         if (r > 0) {
613           final TextView spacer = new TextView(tableRow.getContext());
614           spacer.setText(r == 0 ? "� " : " � ");
615           tableRow.addView(spacer);
616         }
617         tableRow.addView(column2, layoutParams);
618
619         column1.setWidth(1);
620         column2.setWidth(1);
621         // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
622
623         // TODO: color words by gender
624         final String col1Text = entry.getAllText(languageData.lang)[r];
625         column1.setText(col1Text, TextView.BufferType.SPANNABLE);
626         final Spannable col1Spannable = (Spannable) column1.getText();
627         int startPos = 0;
628         final String token = languageData.getIndexEntryForRow(rowIndex).word;
629         while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
630           col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
631               startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
632           startPos += token.length();
633         }
634
635         column2.setText(
636             entry.getAllText(SimpleEntry.otherLang(languageData.lang))[r],
637             TextView.BufferType.NORMAL);
638
639         result.addView(tableRow);
640       }
641
642       return result;
643     }
644
645   } // DictionaryListAdapter
646
647   private final class SearchOperation implements Runnable {
648     SearchOperation previousSearchOperation;
649     
650     final LanguageListAdapter listAdapter;
651     final LanguageData languageData;
652     final String searchText;
653     final AtomicBoolean interrupted = new AtomicBoolean(false);
654     boolean searchFinished = false;
655
656     SearchOperation(final LanguageListAdapter listAdapter,
657         final String searchText, final SearchOperation previousSearchOperation) {
658       this.listAdapter = listAdapter;
659       this.languageData = listAdapter.languageData;
660       this.searchText = searchText;
661       this.previousSearchOperation = previousSearchOperation;
662     }
663
664     public void run() {
665       if (previousSearchOperation != null) {
666         previousSearchOperation.stopAndWait();
667       }
668       previousSearchOperation = null;
669       
670       Log.d(LOG, "SearchOperation: " + searchText);
671       final int indexLocation = languageData.lookup(searchText, interrupted);
672       if (!interrupted.get()) {
673         final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
674         
675         Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
676         uiHandler.post(new Runnable() {
677           public void run() {
678             // Check is just a performance operation.
679             if (!interrupted.get()) {
680               // This is safe, because it checks that the listAdapter hasn't changed.
681               jumpToRow(listAdapter, indexEntry.startRow);
682             }
683             synchronized (DictionaryActivity.this) {
684               searchOperation = null;
685               DictionaryActivity.this.notifyAll();
686             }
687           }
688         });
689       }      
690       synchronized (this) {
691         searchFinished = true;
692         this.notifyAll();
693       }
694     }
695     
696     private void stopAndWait() {
697       interrupted.set(true);
698       synchronized (this) {
699         while (!searchFinished) {
700           Log.d(LOG, "stopAndWait: " + searchText);
701           try {
702             this.wait();
703           } catch (InterruptedException e) {
704             Log.e(LOG, "Interrupted", e);
705           }
706         }
707       }
708     }
709   }  // SearchOperation
710
711   void waitForSearchEnd() {
712     synchronized (this) {
713       while (searchOperation != null) {
714         Log.d(LOG, "waitForSearchEnd");
715         try {
716           this.wait();
717         } catch (InterruptedException e) {
718           Log.e(LOG, "Interrupted.", e);
719         }
720       }
721     }
722   }
723
724   private class SearchTextWatcher implements TextWatcher {
725     public void afterTextChanged(final Editable searchTextEditable) {
726       Log.d(LOG, "Search text changed: " + searchText.getText().toString());
727       if (searchText.hasFocus()) {
728         // If they were typing to cause the change, update the UI.
729         onSearchTextChange(searchText.getText().toString());
730       }
731     }
732
733     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
734         int arg3) {
735     }
736
737     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
738     }
739   }
740
741 }