]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
d217c8191b1010ac7053f7e337cd47821a75882d
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 // Copyright 2011 Google Inc. All Rights Reserved.\r
2 //\r
3 // Licensed under the Apache License, Version 2.0 (the "License");\r
4 // you may not use this file except in compliance with the License.\r
5 // You may obtain a copy of the License at\r
6 //\r
7 //     http://www.apache.org/licenses/LICENSE-2.0\r
8 //\r
9 // Unless required by applicable law or agreed to in writing, software\r
10 // distributed under the License is distributed on an "AS IS" BASIS,\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
12 // See the License for the specific language governing permissions and\r
13 // limitations under the License.\r
14 \r
15 package com.hughes.android.dictionary;\r
16 \r
17 import java.io.File;\r
18 import java.io.FileWriter;\r
19 import java.io.IOException;\r
20 import java.io.PrintWriter;\r
21 import java.io.RandomAccessFile;\r
22 import java.text.SimpleDateFormat;\r
23 import java.util.Date;\r
24 import java.util.concurrent.Executor;\r
25 import java.util.concurrent.Executors;\r
26 import java.util.concurrent.ThreadFactory;\r
27 import java.util.concurrent.atomic.AtomicBoolean;\r
28 \r
29 import android.app.ListActivity;\r
30 import android.content.Context;\r
31 import android.content.Intent;\r
32 import android.content.SharedPreferences;\r
33 import android.graphics.Typeface;\r
34 import android.os.Bundle;\r
35 import android.os.Handler;\r
36 import android.preference.PreferenceManager;\r
37 import android.text.ClipboardManager;\r
38 import android.text.Editable;\r
39 import android.text.Selection;\r
40 import android.text.Spannable;\r
41 import android.text.TextWatcher;\r
42 import android.text.style.StyleSpan;\r
43 import android.util.Log;\r
44 import android.util.TypedValue;\r
45 import android.view.ContextMenu;\r
46 import android.view.ContextMenu.ContextMenuInfo;\r
47 import android.view.KeyEvent;\r
48 import android.view.Menu;\r
49 import android.view.MenuItem;\r
50 import android.view.MenuItem.OnMenuItemClickListener;\r
51 import android.view.View;\r
52 import android.view.View.OnClickListener;\r
53 import android.view.ViewGroup;\r
54 import android.view.inputmethod.InputMethodManager;\r
55 import android.widget.AdapterView;\r
56 import android.widget.AdapterView.AdapterContextMenuInfo;\r
57 import android.widget.BaseAdapter;\r
58 import android.widget.Button;\r
59 import android.widget.EditText;\r
60 import android.widget.ListAdapter;\r
61 import android.widget.ListView;\r
62 import android.widget.TableLayout;\r
63 import android.widget.TableRow;\r
64 import android.widget.TextView;\r
65 import android.widget.Toast;\r
66 \r
67 import com.hughes.android.dictionary.engine.Dictionary;\r
68 import com.hughes.android.dictionary.engine.Index;\r
69 import com.hughes.android.dictionary.engine.PairEntry;\r
70 import com.hughes.android.dictionary.engine.PairEntry.Pair;\r
71 import com.hughes.android.dictionary.engine.RowBase;\r
72 import com.hughes.android.dictionary.engine.TokenRow;\r
73 import com.hughes.android.dictionary.engine.TransliteratorManager;\r
74 import com.hughes.android.util.PersistentObjectCache;\r
75 \r
76 public class DictionaryActivity extends ListActivity {\r
77 \r
78   static final String LOG = "QuickDic";\r
79   \r
80   static final int VIBRATE_MILLIS = 100;\r
81 \r
82   int dictIndex = 0;\r
83   RandomAccessFile dictRaf = null;\r
84   Dictionary dictionary = null;\r
85   int indexIndex = 0;\r
86   Index index = null;\r
87   \r
88   // package for test.\r
89   final Handler uiHandler = new Handler();\r
90   private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {\r
91     @Override\r
92     public Thread newThread(Runnable r) {\r
93       return new Thread(r, "searchExecutor");\r
94     }\r
95   });\r
96   private SearchOperation currentSearchOperation = null;\r
97 \r
98   int fontSizeSp;\r
99   EditText searchText;\r
100   Button langButton;\r
101 \r
102   // Never null.\r
103   private File wordList = null;\r
104   private boolean saveOnlyFirstSubentry = false;\r
105   private boolean clickOpensContextMenu = false;\r
106 \r
107   // Visible for testing.\r
108   ListAdapter indexAdapter = null;\r
109   \r
110   final SearchTextWatcher searchTextWatcher = new SearchTextWatcher();\r
111 \r
112   //private Vibrator vibrator = null;\r
113   \r
114   public DictionaryActivity() {\r
115   }\r
116   \r
117   public static Intent getIntent(final Context context, final int dictIndex, final int indexIndex, final String searchToken) {\r
118     setDictionaryPrefs(context, dictIndex, indexIndex, searchToken);\r
119     \r
120     final Intent intent = new Intent();\r
121     intent.setClassName(DictionaryActivity.class.getPackage().getName(), DictionaryActivity.class.getName());\r
122     return intent;\r
123   }\r
124 \r
125   public static void setDictionaryPrefs(final Context context,\r
126       final int dictIndex, final int indexIndex, final String searchToken) {\r
127     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
128     prefs.putInt(C.DICT_INDEX, dictIndex);\r
129     prefs.putInt(C.INDEX_INDEX, indexIndex);\r
130     prefs.putString(C.SEARCH_TOKEN, searchToken);\r
131     prefs.commit();\r
132   }\r
133 \r
134   public static void clearDictionaryPrefs(final Context context) {\r
135     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
136     prefs.remove(C.DICT_INDEX);\r
137     prefs.remove(C.INDEX_INDEX);\r
138     prefs.remove(C.SEARCH_TOKEN);\r
139     prefs.commit();\r
140     Log.d(LOG, "Removed default dictionary prefs.");\r
141   }\r
142 \r
143   @Override\r
144   public void onCreate(Bundle savedInstanceState) {\r
145     Log.d(LOG, "onCreate:" + this);\r
146     ((DictionaryApplication)getApplication()).applyTheme(this);\r
147     super.onCreate(savedInstanceState);\r
148     \r
149     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);\r
150     \r
151     try {\r
152       PersistentObjectCache.init(this);\r
153       QuickDicConfig quickDicConfig = PersistentObjectCache.init(\r
154           this).read(C.DICTIONARY_CONFIGS, QuickDicConfig.class);\r
155       dictIndex = prefs.getInt(C.DICT_INDEX, 0) ;\r
156       final DictionaryConfig dictionaryConfig = quickDicConfig.dictionaryConfigs.get(dictIndex);\r
157       this.setTitle("QuickDic: " + dictionaryConfig.name);\r
158       dictRaf = new RandomAccessFile(dictionaryConfig.localFile, "r");\r
159       dictionary = new Dictionary(dictRaf); \r
160     } catch (Exception e) {\r
161       Log.e(LOG, "Unable to load dictionary.", e);\r
162       if (dictRaf != null) {\r
163         try {\r
164           dictRaf.close();\r
165         } catch (IOException e1) {\r
166           Log.e(LOG, "Unable to close dictRaf.", e1);\r
167         }\r
168         dictRaf = null;\r
169       }\r
170       Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG);\r
171       startActivity(DictionaryEditActivity.getIntent(dictIndex));\r
172       finish();\r
173       return;\r
174     }\r
175 \r
176     indexIndex = prefs.getInt(C.INDEX_INDEX, 0) % dictionary.indices.size();\r
177     Log.d(LOG, "Loading index.");\r
178     index = dictionary.indices.get(indexIndex);\r
179     setListAdapter(new IndexAdapter(index));\r
180 \r
181     // Pre-load the collators.\r
182     searchExecutor.execute(new Runnable() {\r
183       public void run() {\r
184         final long startMillis = System.currentTimeMillis();\r
185         \r
186         TransliteratorManager.init(new TransliteratorManager.Callback() {\r
187           @Override\r
188           public void onTransliteratorReady() {\r
189             uiHandler.post(new Runnable() {\r
190               @Override\r
191               public void run() {\r
192                 onSearchTextChange(searchText.getText().toString());\r
193               }\r
194             });\r
195           }\r
196         });\r
197         \r
198         for (final Index index : dictionary.indices) {\r
199           Log.d(LOG, "Starting collator load for lang=" + index.sortLanguage.getSymbol());\r
200           \r
201           final com.ibm.icu.text.Collator c = index.sortLanguage.getCollator();          \r
202           if (c.compare("pre-print", "preppy") >= 0) {\r
203             Log.e(LOG, c.getClass()\r
204                 + " is buggy, lookups may not work properly.");\r
205           }\r
206         }\r
207         Log.d(LOG, "Loading collators took:"\r
208             + (System.currentTimeMillis() - startMillis));\r
209       }\r
210     });\r
211     \r
212     final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "12");\r
213     try {\r
214       fontSizeSp = Integer.parseInt(fontSize);\r
215     } catch (NumberFormatException e) {\r
216       fontSizeSp = 12;\r
217     }\r
218 \r
219     setContentView(R.layout.dictionary_activity);\r
220     searchText = (EditText) findViewById(R.id.SearchText);\r
221     searchText.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
222     \r
223     langButton = (Button) findViewById(R.id.LangButton);\r
224     \r
225     searchText.requestFocus();\r
226     searchText.addTextChangedListener(searchTextWatcher);\r
227     final String search = prefs.getString(C.SEARCH_TOKEN, "");\r
228     searchText.setText(search);\r
229     searchText.setSelection(0, search.length());\r
230     Log.d(LOG, "Trying to restore searchText=" + search);\r
231     \r
232     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);\r
233     clearSearchTextButton.setOnClickListener(new OnClickListener() {\r
234       public void onClick(View v) {\r
235         onClearSearchTextButton(clearSearchTextButton);\r
236       }\r
237     });\r
238     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(\r
239         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE\r
240         : View.GONE);\r
241     \r
242     final Button langButton = (Button) findViewById(R.id.LangButton);\r
243     langButton.setOnClickListener(new OnClickListener() {\r
244       public void onClick(View v) {\r
245         onLanguageButton();\r
246       }\r
247     });\r
248     updateLangButton();\r
249     \r
250     final Button upButton = (Button) findViewById(R.id.UpButton);\r
251     upButton.setOnClickListener(new OnClickListener() {\r
252       public void onClick(View v) {\r
253         onUpDownButton(true);\r
254       }\r
255     });\r
256     final Button downButton = (Button) findViewById(R.id.DownButton);\r
257     downButton.setOnClickListener(new OnClickListener() {\r
258       public void onClick(View v) {\r
259         onUpDownButton(false);\r
260       }\r
261     });\r
262 \r
263    getListView().setOnItemSelectedListener(new ListView.OnItemSelectedListener() {\r
264       @Override\r
265       public void onItemSelected(AdapterView<?> adapterView, View arg1, final int position,\r
266           long id) {\r
267         if (!searchText.isFocused()) {\r
268           // TODO: don't do this if multi words are entered.\r
269           final RowBase row = (RowBase) getListAdapter().getItem(position);\r
270           Log.d(LOG, "onItemSelected: " + row.index());\r
271           final TokenRow tokenRow = row.getTokenRow(true);\r
272           searchText.setText(tokenRow.getToken());\r
273         }\r
274       }\r
275 \r
276       @Override\r
277       public void onNothingSelected(AdapterView<?> arg0) {\r
278       }\r
279     });\r
280 \r
281     // ContextMenu.\r
282     registerForContextMenu(getListView());\r
283 \r
284     // Prefs.\r
285     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),\r
286         getString(R.string.wordListFileDefault)));\r
287     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);\r
288     clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey), false);\r
289     //if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) {\r
290       // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);\r
291     //}\r
292     Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);\r
293   }\r
294   \r
295   @Override\r
296   protected void onResume() {\r
297     super.onResume();\r
298   }\r
299   \r
300   @Override\r
301   protected void onPause() {\r
302     super.onPause();\r
303   }\r
304 \r
305   @Override\r
306   protected void onDestroy() {\r
307     super.onDestroy();\r
308     if (dictRaf == null) {\r
309       return;\r
310     }\r
311     setDictionaryPrefs(this, dictIndex, indexIndex, searchText.getText().toString());\r
312     \r
313     // Before we close the RAF, we have to wind the current search down.\r
314     if (currentSearchOperation != null) {\r
315       Log.d(LOG, "Interrupting search to shut down.");\r
316       final SearchOperation searchOperation = currentSearchOperation;\r
317       currentSearchOperation = null;\r
318       searchOperation.interrupted.set(true);\r
319       synchronized (searchOperation) {\r
320         while (!searchOperation.done) {\r
321           try {\r
322             searchOperation.wait();\r
323           } catch (InterruptedException e) {\r
324             Log.d(LOG, "Interrupted.", e);\r
325           }\r
326         }\r
327       }\r
328     }\r
329     \r
330     try {\r
331       Log.d(LOG, "Closing RAF.");\r
332       dictRaf.close();\r
333     } catch (IOException e) {\r
334       Log.e(LOG, "Failed to close dictionary", e);\r
335     }\r
336     dictRaf = null;\r
337   }\r
338 \r
339   // --------------------------------------------------------------------------\r
340   // Buttons\r
341   // --------------------------------------------------------------------------\r
342 \r
343   private void onClearSearchTextButton(final Button clearSearchTextButton) {\r
344     clearSearchTextButton.requestFocus();\r
345     searchText.setText("");\r
346     searchText.requestFocus();\r
347     Log.d(LOG, "Trying to show soft keyboard.");\r
348     final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
349     manager.showSoftInput(searchText, InputMethodManager.SHOW_FORCED);\r
350   }\r
351   \r
352   void updateLangButton() {\r
353     langButton.setText(index.shortName.toUpperCase());\r
354   }\r
355 \r
356   void onLanguageButton() {\r
357     if (currentSearchOperation != null) {\r
358       currentSearchOperation.interrupted.set(true);\r
359       currentSearchOperation = null;\r
360     }\r
361     \r
362     indexIndex = (indexIndex + 1) % dictionary.indices.size();\r
363     index = dictionary.indices.get(indexIndex);\r
364     indexAdapter = new IndexAdapter(index);\r
365     Log.d(LOG, "onLanguageButton, newLang=" + index.longName);\r
366     setListAdapter(indexAdapter);\r
367     updateLangButton();\r
368     onSearchTextChange(searchText.getText().toString());\r
369   }\r
370   \r
371   void onUpDownButton(final boolean up) {\r
372     final int firstVisibleRow = getListView().getFirstVisiblePosition();\r
373     final RowBase row = index.rows.get(firstVisibleRow);\r
374     final TokenRow tokenRow = row.getTokenRow(true);\r
375     final int destIndexEntry;\r
376     if (up) {\r
377       if (row != tokenRow) {\r
378         destIndexEntry = tokenRow.referenceIndex;\r
379       } else {\r
380         destIndexEntry = Math.max(tokenRow.referenceIndex - 1, 0);\r
381       }\r
382     } else {\r
383       // Down\r
384       destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size());\r
385     }\r
386     final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry);\r
387     Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token);\r
388     searchText.removeTextChangedListener(searchTextWatcher);\r
389     searchText.setText(dest.token);\r
390     jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow);\r
391     searchText.addTextChangedListener(searchTextWatcher);\r
392   }\r
393 \r
394   // --------------------------------------------------------------------------\r
395   // Options Menu\r
396   // --------------------------------------------------------------------------\r
397   \r
398   @Override\r
399   public boolean onCreateOptionsMenu(final Menu menu) {\r
400     \r
401     {\r
402       final MenuItem preferences = menu.add(getString(R.string.preferences));\r
403       preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
404         public boolean onMenuItemClick(final MenuItem menuItem) {\r
405           startActivity(new Intent(DictionaryActivity.this,\r
406               PreferenceActivity.class));\r
407           return false;\r
408         }\r
409       });\r
410     }\r
411 \r
412     {\r
413       final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryList));\r
414       dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
415         public boolean onMenuItemClick(final MenuItem menuItem) {\r
416           startActivity(DictionaryListActivity.getIntent(DictionaryActivity.this));\r
417           finish();\r
418           return false;\r
419         }\r
420       });\r
421     }\r
422 \r
423     {\r
424       final MenuItem dictionaryEdit = menu.add(getString(R.string.editDictionary));\r
425       dictionaryEdit.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
426         public boolean onMenuItemClick(final MenuItem menuItem) {\r
427           final Intent intent = DictionaryEditActivity.getIntent(dictIndex);\r
428           startActivity(intent);\r
429           return false;\r
430         }\r
431       });\r
432     }\r
433 \r
434     {\r
435       final MenuItem about = menu.add(getString(R.string.about));\r
436       about.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
437         public boolean onMenuItemClick(final MenuItem menuItem) {\r
438           final Intent intent = new Intent().setClassName(AboutActivity.class\r
439               .getPackage().getName(), AboutActivity.class.getCanonicalName());\r
440           startActivity(intent);\r
441           return false;\r
442         }\r
443       });\r
444     }\r
445 \r
446     return true;\r
447   }\r
448 \r
449 \r
450   // --------------------------------------------------------------------------\r
451   // Context Menu + clicks\r
452   // --------------------------------------------------------------------------\r
453 \r
454   @Override\r
455   public void onCreateContextMenu(ContextMenu menu, View v,\r
456       ContextMenuInfo menuInfo) {\r
457     AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;\r
458     final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);\r
459 \r
460     final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));\r
461     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
462       public boolean onMenuItemClick(MenuItem item) {\r
463         onAppendToWordList(row);\r
464         return false;\r
465       }\r
466     });\r
467 \r
468     final MenuItem copy = menu.add(android.R.string.copy);\r
469     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
470       public boolean onMenuItemClick(MenuItem item) {\r
471         onCopy(row);\r
472         return false;\r
473       }\r
474     });\r
475 \r
476   }\r
477   \r
478   @Override\r
479   protected void onListItemClick(ListView l, View v, int row, long id) {\r
480     defocusSearchText();\r
481     \r
482     if (clickOpensContextMenu && dictRaf != null) {\r
483       openContextMenu(v);\r
484     }\r
485   }\r
486   \r
487   void onAppendToWordList(final RowBase row) {\r
488     defocusSearchText();\r
489     \r
490     final StringBuilder rawText = new StringBuilder();\r
491     rawText.append(\r
492         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))\r
493         .append("\t");\r
494     rawText.append(index.longName).append("\t");\r
495     rawText.append(row.getTokenRow(true).getToken()).append("\t");\r
496     rawText.append(row.getRawText(saveOnlyFirstSubentry));\r
497     Log.d(LOG, "Writing : " + rawText);\r
498 \r
499     try {\r
500       wordList.getParentFile().mkdirs();\r
501       final PrintWriter out = new PrintWriter(\r
502           new FileWriter(wordList, true));\r
503       out.println(rawText.toString());\r
504       out.close();\r
505     } catch (IOException e) {\r
506       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);\r
507       Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);\r
508     }\r
509     return;\r
510   }\r
511   \r
512   /**\r
513    * Called when user clicks outside of search text, so that they can start\r
514    * typing again immediately.\r
515    */\r
516   void defocusSearchText() {\r
517     //Log.d(LOG, "defocusSearchText");\r
518     // Request focus so that if we start typing again, it clears the text input.\r
519     getListView().requestFocus();\r
520     \r
521     // Visual indication that a new keystroke will clear the search text.\r
522     searchText.selectAll();\r
523   }\r
524 \r
525   void onCopy(final RowBase row) {\r
526     defocusSearchText();\r
527 \r
528     Log.d(LOG, "Copy, row=" + row);\r
529     final StringBuilder result = new StringBuilder();\r
530     result.append(row.getRawText(false));\r
531     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\r
532     clipboardManager.setText(result.toString());\r
533     Log.d(LOG, "Copied: " + result);\r
534   }\r
535 \r
536   @Override\r
537   public boolean onKeyDown(final int keyCode, final KeyEvent event) {\r
538     if (event.getUnicodeChar() != 0) {\r
539       if (!searchText.hasFocus()) {\r
540         searchText.setText("" + (char) event.getUnicodeChar());\r
541         onSearchTextChange(searchText.getText().toString());\r
542         searchText.requestFocus();\r
543         Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
544       }\r
545       return true;\r
546     }\r
547     if (keyCode == KeyEvent.KEYCODE_BACK) {\r
548       Log.d(LOG, "Clearing dictionary prefs.");\r
549       DictionaryActivity.clearDictionaryPrefs(this);\r
550     }\r
551     if (keyCode == KeyEvent.KEYCODE_ENTER) {\r
552 //      Log.d(LOG, "Trying to hide soft keyboard.");\r
553 //      final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
554 //      manager.hideSoftInputFromWindow(searchText, InputMethodManager.SHOW_FORCED);\r
555 \r
556     }\r
557     return super.onKeyDown(keyCode, event);\r
558   }\r
559 \r
560 \r
561   // --------------------------------------------------------------------------\r
562   // SearchOperation\r
563   // --------------------------------------------------------------------------\r
564 \r
565   private void searchFinished(final SearchOperation searchOperation) {\r
566     if (searchOperation.interrupted.get()) {\r
567       Log.d(LOG, "Search operation was interrupted: " + searchOperation);\r
568       return;\r
569     }\r
570     if (searchOperation != this.currentSearchOperation) {\r
571       Log.d(LOG, "Stale searchOperation finished: " + searchOperation);\r
572       return;\r
573     }\r
574     \r
575     final Index.IndexEntry searchResult = searchOperation.searchResult;\r
576     Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult);\r
577 \r
578     currentSearchOperation = null;\r
579 \r
580     uiHandler.postDelayed(new Runnable() {\r
581       @Override\r
582       public void run() {\r
583         if (currentSearchOperation == null) {\r
584           jumpToRow(searchResult.startRow);\r
585         } else {\r
586           Log.d(LOG, "More coming, waiting for currentSearchOperation.");\r
587         }\r
588       }\r
589     }, 50);\r
590     \r
591 //    if (!searchResult.success) {\r
592 //      if (vibrator != null) {\r
593 //        vibrator.vibrate(VIBRATE_MILLIS);\r
594 //      }\r
595 //      searchText.setText(searchResult.longestPrefixString);\r
596 //      searchText.setSelection(searchResult.longestPrefixString.length());\r
597 //      return;\r
598 //    }\r
599     \r
600   }\r
601   \r
602   private final void jumpToRow(final int row) {\r
603     setSelection(row);\r
604     getListView().setSelected(true);\r
605   }\r
606 \r
607   final class SearchOperation implements Runnable {\r
608     \r
609     final AtomicBoolean interrupted = new AtomicBoolean(false);\r
610     final String searchText;\r
611     final Index index;\r
612     \r
613     long searchStartMillis;\r
614 \r
615     Index.IndexEntry searchResult;\r
616     \r
617     boolean done = false;\r
618     \r
619     SearchOperation(final String searchText, final Index index) {\r
620       this.searchText = searchText.trim();\r
621       this.index = index;\r
622     }\r
623     \r
624     public String toString() {\r
625       return String.format("SearchOperation(%s,%s)", searchText, interrupted.toString());\r
626     }\r
627 \r
628     @Override\r
629     public void run() {\r
630       try {\r
631         searchStartMillis = System.currentTimeMillis();\r
632         searchResult = index.findInsertionPoint(searchText, interrupted);\r
633         Log.d(LOG, "searchText=" + searchText + ", searchDuration="\r
634             + (System.currentTimeMillis() - searchStartMillis) + ", interrupted="\r
635             + interrupted.get());\r
636         if (!interrupted.get()) {\r
637           uiHandler.post(new Runnable() {\r
638             @Override\r
639             public void run() {            \r
640               searchFinished(SearchOperation.this);\r
641             }\r
642           });\r
643         }\r
644       } finally {\r
645         synchronized (this) {\r
646           done = true;\r
647           this.notifyAll();\r
648         }\r
649       }\r
650     }\r
651   }\r
652 \r
653   \r
654   // --------------------------------------------------------------------------\r
655   // IndexAdapter\r
656   // --------------------------------------------------------------------------\r
657 \r
658   final class IndexAdapter extends BaseAdapter {\r
659     \r
660     final Index index;\r
661 \r
662     IndexAdapter(final Index index) {\r
663       this.index = index;\r
664     }\r
665 \r
666     @Override\r
667     public int getCount() {\r
668       return index.rows.size();\r
669     }\r
670 \r
671     @Override\r
672     public RowBase getItem(int position) {\r
673       return index.rows.get(position);\r
674     }\r
675 \r
676     @Override\r
677     public long getItemId(int position) {\r
678       return getItem(position).index();\r
679     }\r
680 \r
681     @Override\r
682     public View getView(int position, View convertView, ViewGroup parent) {\r
683       final RowBase row = index.rows.get(position);\r
684       if (row instanceof PairEntry.Row) {\r
685         return getView((PairEntry.Row) row, parent);\r
686       } else if (row instanceof TokenRow) {\r
687         return getView((TokenRow) row, parent);\r
688       } else {\r
689         throw new IllegalArgumentException("Unsupported Row type: " + row.getClass());\r
690       }\r
691     }\r
692 \r
693     private View getView(PairEntry.Row row, ViewGroup parent) {\r
694       final TableLayout result = new TableLayout(parent.getContext());\r
695       final PairEntry entry = row.getEntry();\r
696       final int rowCount = entry.pairs.size();\r
697       for (int r = 0; r < rowCount; ++r) {\r
698         final TableRow tableRow = new TableRow(result.getContext());\r
699 \r
700         TextView column1 = new TextView(tableRow.getContext());\r
701         TextView column2 = new TextView(tableRow.getContext());\r
702         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();\r
703         layoutParams.weight = 0.5f;\r
704 \r
705         if (r > 0) {\r
706           final TextView spacer = new TextView(tableRow.getContext());\r
707           spacer.setText(" • ");\r
708           tableRow.addView(spacer);\r
709         }\r
710         tableRow.addView(column1, layoutParams);\r
711         if (r > 0) {\r
712           final TextView spacer = new TextView(tableRow.getContext());\r
713           spacer.setText(" • ");\r
714           tableRow.addView(spacer);\r
715         }\r
716         tableRow.addView(column2, layoutParams);\r
717 \r
718         column1.setWidth(1);\r
719         column2.setWidth(1);\r
720 \r
721         // TODO: color words by gender\r
722         final Pair pair = entry.pairs.get(r);\r
723         final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1;\r
724         column1.setText(col1Text, TextView.BufferType.SPANNABLE);\r
725         final Spannable col1Spannable = (Spannable) column1.getText();\r
726         \r
727         int startPos = 0;\r
728         final String token = row.getTokenRow(true).getToken();\r
729         while ((startPos = col1Text.indexOf(token, startPos)) != -1) {\r
730           col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,\r
731               startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
732           startPos += token.length();\r
733         }\r
734 \r
735         final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;\r
736         column2.setText(col2Text, TextView.BufferType.NORMAL);\r
737         \r
738         column1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
739         column2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
740 \r
741         result.addView(tableRow);\r
742       }\r
743 \r
744       return result;\r
745     }\r
746 \r
747     private View getView(TokenRow row, ViewGroup parent) {\r
748       final TextView textView = new TextView(parent.getContext());\r
749       textView.setText(row.getToken());\r
750       textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 5 * fontSizeSp / 4);\r
751       return textView;\r
752     }\r
753     \r
754   }\r
755 \r
756   // --------------------------------------------------------------------------\r
757   // SearchText\r
758   // --------------------------------------------------------------------------\r
759 \r
760   void onSearchTextChange(final String text) {\r
761     if (dictRaf == null) {\r
762       Log.d(LOG, "searchText changed during shutdown, doing nothing.");\r
763       return;\r
764     }\r
765     if (!searchText.isFocused()) {\r
766       Log.d(LOG, "searchText changed without focus, doing nothing.");\r
767       return;\r
768     }\r
769     Log.d(LOG, "onSearchTextChange: " + text);    \r
770     if (currentSearchOperation != null) {\r
771       Log.d(LOG, "Interrupting currentSearchOperation.");\r
772       currentSearchOperation.interrupted.set(true);\r
773     }\r
774     currentSearchOperation = new SearchOperation(text, index);\r
775     searchExecutor.execute(currentSearchOperation);\r
776   }\r
777   \r
778   private class SearchTextWatcher implements TextWatcher {\r
779     public void afterTextChanged(final Editable searchTextEditable) {\r
780       if (searchText.hasFocus()) {\r
781         Log.d(LOG, "Search text changed with focus: " + searchText.getText());\r
782         // If they were typing to cause the change, update the UI.\r
783         onSearchTextChange(searchText.getText().toString());\r
784       }\r
785     }\r
786 \r
787     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,\r
788         int arg3) {\r
789     }\r
790 \r
791     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {\r
792     }\r
793   }\r
794 \r
795 }\r