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