]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
2ecaf3738615785f7f876a837c3e47d92f7d7259
[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.Arrays;\r
24 import java.util.Collections;\r
25 import java.util.Date;\r
26 import java.util.LinkedHashSet;\r
27 import java.util.List;\r
28 import java.util.Set;\r
29 import java.util.concurrent.Executor;\r
30 import java.util.concurrent.Executors;\r
31 import java.util.concurrent.ThreadFactory;\r
32 import java.util.concurrent.atomic.AtomicBoolean;\r
33 import java.util.regex.Matcher;\r
34 import java.util.regex.Pattern;\r
35 \r
36 import android.app.Dialog;\r
37 import android.app.ListActivity;\r
38 import android.content.Context;\r
39 import android.content.Intent;\r
40 import android.content.SharedPreferences;\r
41 import android.graphics.Typeface;\r
42 import android.net.Uri;\r
43 import android.os.Bundle;\r
44 import android.os.Handler;\r
45 import android.preference.PreferenceManager;\r
46 import android.text.ClipboardManager;\r
47 import android.text.Editable;\r
48 import android.text.Selection;\r
49 import android.text.Spannable;\r
50 import android.text.TextWatcher;\r
51 import android.text.method.LinkMovementMethod;\r
52 import android.text.style.StyleSpan;\r
53 import android.util.Log;\r
54 import android.util.TypedValue;\r
55 import android.view.ContextMenu;\r
56 import android.view.ContextMenu.ContextMenuInfo;\r
57 import android.view.KeyEvent;\r
58 import android.view.Menu;\r
59 import android.view.MenuItem;\r
60 import android.view.MenuItem.OnMenuItemClickListener;\r
61 import android.view.MotionEvent;\r
62 import android.view.View;\r
63 import android.view.View.OnClickListener;\r
64 import android.view.View.OnLongClickListener;\r
65 import android.view.ViewGroup;\r
66 import android.view.WindowManager;\r
67 import android.view.inputmethod.InputMethodManager;\r
68 import android.widget.AdapterView;\r
69 import android.widget.AdapterView.AdapterContextMenuInfo;\r
70 import android.widget.BaseAdapter;\r
71 import android.widget.Button;\r
72 import android.widget.EditText;\r
73 import android.widget.ImageView;\r
74 import android.widget.LinearLayout;\r
75 import android.widget.ListAdapter;\r
76 import android.widget.ListView;\r
77 import android.widget.TableLayout;\r
78 import android.widget.TableRow;\r
79 import android.widget.TextView;\r
80 import android.widget.Toast;\r
81 \r
82 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;\r
83 import com.hughes.android.dictionary.engine.Dictionary;\r
84 import com.hughes.android.dictionary.engine.EntrySource;\r
85 import com.hughes.android.dictionary.engine.Index;\r
86 import com.hughes.android.dictionary.engine.Index.IndexEntry;\r
87 import com.hughes.android.dictionary.engine.PairEntry;\r
88 import com.hughes.android.dictionary.engine.PairEntry.Pair;\r
89 import com.hughes.android.dictionary.engine.RowBase;\r
90 import com.hughes.android.dictionary.engine.TokenRow;\r
91 import com.hughes.android.dictionary.engine.TransliteratorManager;\r
92 import com.hughes.android.util.IntentLauncher;\r
93 import com.hughes.android.util.NonLinkClickableSpan;\r
94 \r
95 public class DictionaryActivity extends ListActivity {\r
96 \r
97   static final String LOG = "QuickDic";\r
98 \r
99   private String initialSearchText;\r
100 \r
101   DictionaryApplication application;\r
102   File dictFile = null;\r
103   RandomAccessFile dictRaf = null;\r
104   Dictionary dictionary = null;\r
105   int indexIndex = 0;\r
106   Index index = null;\r
107   List<RowBase> rowsToShow = null;  // if not null, just show these rows.\r
108   \r
109   // package for test.\r
110   final Handler uiHandler = new Handler();\r
111   private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {\r
112     @Override\r
113     public Thread newThread(Runnable r) {\r
114       return new Thread(r, "searchExecutor");\r
115     }\r
116   });\r
117   private SearchOperation currentSearchOperation = null;\r
118 \r
119   C.Theme theme = C.Theme.LIGHT;\r
120   int fontSizeSp;\r
121   EditText searchText;\r
122   Button langButton;\r
123 \r
124   // Never null.\r
125   private File wordList = null;\r
126   private boolean saveOnlyFirstSubentry = false;\r
127   private boolean clickOpensContextMenu = false;\r
128 \r
129   // Visible for testing.\r
130   ListAdapter indexAdapter = null;\r
131   \r
132   final SearchTextWatcher searchTextWatcher = new SearchTextWatcher();\r
133   \r
134   /**\r
135    * For some languages, loading the transliterators used in this search takes\r
136    * a long time, so we fire it up on a different thread, and don't invoke it\r
137    * from the main thread until it's already finished once.\r
138    */\r
139   private volatile boolean indexPrepFinished = false;\r
140 \r
141 \r
142 \r
143   public DictionaryActivity() {\r
144   }\r
145   \r
146   public static Intent getLaunchIntent(final File dictFile, final int indexIndex, final String searchToken) {\r
147     final Intent intent = new Intent();\r
148     intent.setClassName(DictionaryActivity.class.getPackage().getName(), DictionaryActivity.class.getName());\r
149     intent.putExtra(C.DICT_FILE, dictFile.getPath());\r
150     intent.putExtra(C.INDEX_INDEX, indexIndex);\r
151     intent.putExtra(C.SEARCH_TOKEN, searchToken);\r
152     return intent;\r
153   }\r
154   \r
155   @Override\r
156   protected void onSaveInstanceState(final Bundle outState) {\r
157     super.onSaveInstanceState(outState);\r
158     Log.d(LOG, "onSaveInstanceState: " + searchText.getText().toString());\r
159     outState.putInt(C.INDEX_INDEX, indexIndex);\r
160     outState.putString(C.SEARCH_TOKEN, searchText.getText().toString());\r
161   }\r
162 \r
163   @Override\r
164   protected void onRestoreInstanceState(final Bundle outState) {\r
165     super.onRestoreInstanceState(outState);\r
166     Log.d(LOG, "onRestoreInstanceState: " + outState.getString(C.SEARCH_TOKEN));\r
167     onCreate(outState);\r
168   }\r
169 \r
170   @Override\r
171   public void onCreate(Bundle savedInstanceState) { \r
172     setTheme(((DictionaryApplication)getApplication()).getSelectedTheme().themeId);\r
173 \r
174     Log.d(LOG, "onCreate:" + this);\r
175     super.onCreate(savedInstanceState);\r
176 \r
177     application = (DictionaryApplication) getApplication();\r
178     theme = application.getSelectedTheme();\r
179     \r
180     final Intent intent = getIntent();\r
181     dictFile = new File(intent.getStringExtra(C.DICT_FILE));\r
182     \r
183     try {\r
184       final String name = application.getDictionaryName(dictFile.getName());\r
185       this.setTitle("QuickDic: " + name);\r
186       dictRaf = new RandomAccessFile(dictFile, "r");\r
187       dictionary = new Dictionary(dictRaf); \r
188     } catch (Exception e) {\r
189       Log.e(LOG, "Unable to load dictionary.", e);\r
190       if (dictRaf != null) {\r
191         try {\r
192           dictRaf.close();\r
193         } catch (IOException e1) {\r
194           Log.e(LOG, "Unable to close dictRaf.", e1);\r
195         }\r
196         dictRaf = null;\r
197       }\r
198       Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()), Toast.LENGTH_LONG).show();\r
199       startActivity(DictionaryManagerActivity.getLaunchIntent());\r
200       finish();\r
201       return;\r
202     }\r
203     indexIndex = intent.getIntExtra(C.INDEX_INDEX, 0);\r
204     if (savedInstanceState != null) {\r
205       indexIndex = savedInstanceState.getInt(C.INDEX_INDEX, indexIndex);\r
206     }\r
207     indexIndex %= dictionary.indices.size();\r
208     Log.d(LOG, "Loading index " + indexIndex);\r
209     index = dictionary.indices.get(indexIndex);\r
210     setListAdapter(new IndexAdapter(index));\r
211     \r
212     // Pre-load the collators.\r
213     new Thread(new Runnable() {\r
214       public void run() {\r
215         final long startMillis = System.currentTimeMillis();\r
216         try {\r
217           TransliteratorManager.init(new TransliteratorManager.Callback() {\r
218             @Override\r
219             public void onTransliteratorReady() {\r
220               uiHandler.post(new Runnable() {\r
221                 @Override\r
222                 public void run() {\r
223                   onSearchTextChange(searchText.getText().toString());\r
224                 }\r
225               });\r
226             }\r
227           });\r
228           \r
229           for (final Index index : dictionary.indices) {\r
230             final String searchToken = index.sortedIndexEntries.get(0).token;\r
231             final IndexEntry entry = index.findExact(searchToken);\r
232             if (!searchToken.equals(entry.token)) {\r
233               Log.e(LOG, "Couldn't find token: " + searchToken + ", " + entry.token);\r
234             }\r
235           }\r
236           indexPrepFinished = true;\r
237         } catch (Exception e) {\r
238           Log.w(LOG, "Exception while prepping.  This can happen if dictionary is closed while search is happening.");\r
239         }\r
240           Log.d(LOG, "Prepping indices took:"\r
241               + (System.currentTimeMillis() - startMillis));\r
242       }\r
243     }).start();\r
244     \r
245     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);\r
246     \r
247     final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "14");\r
248     try {\r
249       fontSizeSp = Integer.parseInt(fontSize.trim());\r
250     } catch (NumberFormatException e) {\r
251       fontSizeSp = 12;\r
252     }\r
253 \r
254     setContentView(R.layout.dictionary_activity);\r
255     searchText = (EditText) findViewById(R.id.SearchText);\r
256     searchText.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
257     \r
258     langButton = (Button) findViewById(R.id.LangButton);\r
259     \r
260     searchText.requestFocus();\r
261     searchText.addTextChangedListener(searchTextWatcher);\r
262     String text = "";\r
263     if (savedInstanceState != null) {\r
264       text = savedInstanceState.getString(C.SEARCH_TOKEN);\r
265       if (text == null) {\r
266         text = "";\r
267       }\r
268     }\r
269     setSearchText(text, true);\r
270     Log.d(LOG, "Trying to restore searchText=" + text);\r
271     \r
272     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);\r
273     clearSearchTextButton.setOnClickListener(new OnClickListener() {\r
274       public void onClick(View v) {\r
275         onClearSearchTextButton(clearSearchTextButton);\r
276       }\r
277     });\r
278     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(\r
279         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE\r
280         : View.GONE);\r
281     \r
282     final Button langButton = (Button) findViewById(R.id.LangButton);\r
283     langButton.setOnClickListener(new OnClickListener() {\r
284       public void onClick(View v) {\r
285         onLanguageButton();\r
286       }\r
287     });\r
288     langButton.setOnLongClickListener(new OnLongClickListener() {\r
289       @Override\r
290       public boolean onLongClick(View v) {\r
291         onLanguageButtonLongClick(v.getContext());\r
292         return true;\r
293       }\r
294     });\r
295     updateLangButton();\r
296     \r
297     final Button upButton = (Button) findViewById(R.id.UpButton);\r
298     upButton.setOnClickListener(new OnClickListener() {\r
299       public void onClick(View v) {\r
300         onUpDownButton(true);\r
301       }\r
302     });\r
303     final Button downButton = (Button) findViewById(R.id.DownButton);\r
304     downButton.setOnClickListener(new OnClickListener() {\r
305       public void onClick(View v) {\r
306         onUpDownButton(false);\r
307       }\r
308     });\r
309 \r
310    getListView().setOnItemSelectedListener(new ListView.OnItemSelectedListener() {\r
311       @Override\r
312       public void onItemSelected(AdapterView<?> adapterView, View arg1, final int position,\r
313           long id) {\r
314         if (!searchText.isFocused()) {\r
315           if (!isFiltered()) {\r
316             final RowBase row = (RowBase) getListAdapter().getItem(position);\r
317             Log.d(LOG, "onItemSelected: " + row.index());\r
318             final TokenRow tokenRow = row.getTokenRow(true);\r
319             searchText.setText(tokenRow.getToken());\r
320           }\r
321         }\r
322       }\r
323 \r
324       @Override\r
325       public void onNothingSelected(AdapterView<?> arg0) {\r
326       }\r
327     });\r
328 \r
329     // ContextMenu.\r
330     registerForContextMenu(getListView());\r
331 \r
332     // Prefs.\r
333     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),\r
334         getString(R.string.wordListFileDefault)));\r
335     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);\r
336     clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey), false);\r
337     //if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey), true)) {\r
338       // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);\r
339     //}\r
340     Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);\r
341     \r
342     setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());\r
343   }\r
344   \r
345   @Override\r
346   protected void onResume() {\r
347     super.onResume();\r
348     if (PreferenceActivity.prefsMightHaveChanged) {\r
349       PreferenceActivity.prefsMightHaveChanged = false;\r
350       finish();\r
351       startActivity(getIntent());\r
352     }\r
353     if (initialSearchText != null) {\r
354       setSearchText(initialSearchText, true);\r
355     }\r
356   }\r
357   \r
358   @Override\r
359   protected void onPause() {\r
360     super.onPause();\r
361   }\r
362   \r
363   private static void setDictionaryPrefs(final Context context,\r
364       final File dictFile, final int indexIndex, final String searchToken) {\r
365     final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit();\r
366     prefs.putString(C.DICT_FILE, dictFile.getPath());\r
367     prefs.putInt(C.INDEX_INDEX, indexIndex);\r
368     prefs.putString(C.SEARCH_TOKEN, searchToken);\r
369     prefs.commit();\r
370   }\r
371 \r
372   @Override\r
373   protected void onDestroy() {\r
374     super.onDestroy();\r
375     if (dictRaf == null) {\r
376       return;\r
377     }\r
378 \r
379     final SearchOperation searchOperation = currentSearchOperation;\r
380     currentSearchOperation = null;\r
381 \r
382     // Before we close the RAF, we have to wind the current search down.\r
383     if (searchOperation != null) {\r
384       Log.d(LOG, "Interrupting search to shut down.");\r
385       currentSearchOperation = null;\r
386       searchOperation.interrupted.set(true);\r
387     }\r
388     \r
389     try {\r
390       Log.d(LOG, "Closing RAF.");\r
391       dictRaf.close();\r
392     } catch (IOException e) {\r
393       Log.e(LOG, "Failed to close dictionary", e);\r
394     }\r
395     dictRaf = null;\r
396   }\r
397 \r
398   // --------------------------------------------------------------------------\r
399   // Buttons\r
400   // --------------------------------------------------------------------------\r
401 \r
402   private void onClearSearchTextButton(final Button clearSearchTextButton) {\r
403     setSearchText("", true);\r
404     Log.d(LOG, "Trying to show soft keyboard.");\r
405     final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
406     manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);\r
407   }\r
408   \r
409   void updateLangButton() {\r
410 //    final LanguageResources languageResources = Language.isoCodeToResources.get(index.shortName);\r
411 //    if (languageResources != null && languageResources.flagId != 0) {\r
412 //      langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, languageResources.flagId, 0);\r
413 //    } else {\r
414 //      langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);\r
415       langButton.setText(index.shortName);\r
416 //    }\r
417   }\r
418 \r
419   void onLanguageButton() {\r
420     if (currentSearchOperation != null) {\r
421       currentSearchOperation.interrupted.set(true);\r
422       currentSearchOperation = null;\r
423     }\r
424     changeIndexGetFocusAndResearch((indexIndex + 1)% dictionary.indices.size());\r
425   }\r
426   \r
427   void onLanguageButtonLongClick(final Context context) {\r
428     final Dialog dialog = new Dialog(context);\r
429     dialog.setContentView(R.layout.select_dictionary_dialog);\r
430     dialog.setTitle(R.string.selectDictionary);\r
431 \r
432     final List<DictionaryInfo> installedDicts = ((DictionaryApplication)getApplication()).getUsableDicts();\r
433     ListView listView = (ListView) dialog.findViewById(android.R.id.list);\r
434     listView.setAdapter(new BaseAdapter() {\r
435       @Override\r
436       public View getView(int position, View convertView, ViewGroup parent) {\r
437         final LinearLayout result = new LinearLayout(parent.getContext());\r
438         final DictionaryInfo dictionaryInfo = getItem(position);\r
439           final Button button = new Button(parent.getContext());\r
440           final String name = application.getDictionaryName(dictionaryInfo.uncompressedFilename);\r
441           button.setText(name);\r
442           final IntentLauncher intentLauncher = new IntentLauncher(parent.getContext(), getLaunchIntent(application.getPath(dictionaryInfo.uncompressedFilename), 0, "")) {\r
443             @Override\r
444             protected void onGo() {\r
445               dialog.dismiss();\r
446               DictionaryActivity.this.finish();\r
447             };\r
448           };\r
449           button.setOnClickListener(intentLauncher);\r
450           \r
451           final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\r
452           layoutParams.width = 0;\r
453           layoutParams.weight = 1.0f;\r
454           button.setLayoutParams(layoutParams);\r
455 \r
456           result.addView(button);\r
457         return result;\r
458       }\r
459       \r
460       @Override\r
461       public long getItemId(int position) {\r
462         return position;\r
463       }\r
464       \r
465       @Override\r
466       public DictionaryInfo getItem(int position) {\r
467         return installedDicts.get(position);\r
468       }\r
469       \r
470       @Override\r
471       public int getCount() {\r
472         return installedDicts.size();\r
473       }\r
474     });\r
475     \r
476     dialog.show();\r
477   }\r
478 \r
479 \r
480   private void changeIndexGetFocusAndResearch(final int newIndex) {\r
481     indexIndex = newIndex;\r
482     index = dictionary.indices.get(indexIndex);\r
483     indexAdapter = new IndexAdapter(index);\r
484     Log.d(LOG, "changingIndex, newLang=" + index.longName);\r
485     setListAdapter(indexAdapter);\r
486     updateLangButton();\r
487     searchText.requestFocus();  // Otherwise, nothing may happen.\r
488     onSearchTextChange(searchText.getText().toString());\r
489     setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());\r
490   }\r
491   \r
492   void onUpDownButton(final boolean up) {\r
493     if (isFiltered()) {\r
494       return;\r
495     }\r
496     final int firstVisibleRow = getListView().getFirstVisiblePosition();\r
497     final RowBase row = index.rows.get(firstVisibleRow);\r
498     final TokenRow tokenRow = row.getTokenRow(true);\r
499     final int destIndexEntry;\r
500     if (up) {\r
501       if (row != tokenRow) {\r
502         destIndexEntry = tokenRow.referenceIndex;\r
503       } else {\r
504         destIndexEntry = Math.max(tokenRow.referenceIndex - 1, 0);\r
505       }\r
506     } else {\r
507       // Down\r
508       destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size());\r
509     }\r
510     final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry);\r
511     Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token);\r
512     searchText.removeTextChangedListener(searchTextWatcher);\r
513     searchText.setText(dest.token);\r
514     if (searchText.getLayout() != null) {\r
515       // Surprising, but this can otherwise crash sometimes...\r
516       Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
517     }\r
518     jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow);\r
519     searchText.addTextChangedListener(searchTextWatcher);\r
520   }\r
521 \r
522   // --------------------------------------------------------------------------\r
523   // Options Menu\r
524   // --------------------------------------------------------------------------\r
525   \r
526   @Override\r
527   public boolean onCreateOptionsMenu(final Menu menu) {\r
528     application.onCreateGlobalOptionsMenu(this, menu);\r
529 \r
530     {\r
531       final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryManager));\r
532       dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
533         public boolean onMenuItemClick(final MenuItem menuItem) {\r
534           startActivity(DictionaryManagerActivity.getLaunchIntent());\r
535           finish();\r
536           return false;\r
537         }\r
538       });\r
539     }\r
540 \r
541     {\r
542       final MenuItem aboutDictionary = menu.add(getString(R.string.aboutDictionary));\r
543       aboutDictionary.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
544         public boolean onMenuItemClick(final MenuItem menuItem) {\r
545           final Context context = getListView().getContext();\r
546           final Dialog dialog = new Dialog(context);\r
547           dialog.setContentView(R.layout.about_dictionary_dialog);\r
548           final TextView textView = (TextView) dialog.findViewById(R.id.text);\r
549 \r
550           final String name = application.getDictionaryName(dictFile.getName());\r
551           dialog.setTitle(name);\r
552           \r
553           final StringBuilder builder = new StringBuilder();\r
554           final DictionaryInfo dictionaryInfo = dictionary.getDictionaryInfo();\r
555           dictionaryInfo.uncompressedBytes = dictFile.length();\r
556           if (dictionaryInfo != null) {\r
557             builder.append(dictionaryInfo.dictInfo).append("\n\n");\r
558             builder.append(getString(R.string.dictionaryPath, dictFile.getPath())).append("\n");\r
559             builder.append(getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes)).append("\n");\r
560             builder.append(getString(R.string.dictionaryCreationTime, dictionaryInfo.creationMillis)).append("\n");\r
561             for (final IndexInfo indexInfo : dictionaryInfo.indexInfos) {\r
562               builder.append("\n");\r
563               builder.append(getString(R.string.indexName, indexInfo.shortName)).append("\n");\r
564               builder.append(getString(R.string.mainTokenCount, indexInfo.mainTokenCount)).append("\n");\r
565             }\r
566             builder.append("\n");\r
567             builder.append(getString(R.string.sources)).append("\n");\r
568             for (final EntrySource source : dictionary.sources) {\r
569               builder.append(getString(R.string.sourceInfo, source.getName(), source.getNumEntries())).append("\n");\r
570             }\r
571           }\r
572 //          } else {\r
573 //            builder.append(getString(R.string.invalidDictionary));\r
574 //          }\r
575           textView.setText(builder.toString());\r
576           \r
577           dialog.show();\r
578           final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();\r
579           layoutParams.width = WindowManager.LayoutParams.FILL_PARENT;\r
580           layoutParams.height = WindowManager.LayoutParams.FILL_PARENT;\r
581           dialog.getWindow().setAttributes(layoutParams);\r
582           return false;\r
583         }\r
584       });\r
585     }\r
586 \r
587     return true;\r
588   }\r
589 \r
590 \r
591   // --------------------------------------------------------------------------\r
592   // Context Menu + clicks\r
593   // --------------------------------------------------------------------------\r
594 \r
595   @Override\r
596   public void onCreateContextMenu(ContextMenu menu, View v,\r
597       ContextMenuInfo menuInfo) {\r
598     AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;\r
599     final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);\r
600 \r
601     final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));\r
602     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
603       public boolean onMenuItemClick(MenuItem item) {\r
604         onAppendToWordList(row);\r
605         return false;\r
606       }\r
607     });\r
608 \r
609     final MenuItem copy = menu.add(android.R.string.copy);\r
610     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
611       public boolean onMenuItemClick(MenuItem item) {\r
612         onCopy(row);\r
613         return false;\r
614       }\r
615     });\r
616     \r
617     if (selectedSpannableText != null) {\r
618       final String selectedText = selectedSpannableText;\r
619       final MenuItem searchForSelection = menu.add(getString(R.string.searchForSelection, selectedSpannableText));\r
620       searchForSelection.setOnMenuItemClickListener(new OnMenuItemClickListener() {\r
621         public boolean onMenuItemClick(MenuItem item) {\r
622           int indexToUse = -1;\r
623           for (int i = 0; i < dictionary.indices.size(); ++i) {\r
624             final Index index = dictionary.indices.get(i);\r
625             if (indexPrepFinished) {\r
626               System.out.println("Doing index lookup: on " + selectedText);\r
627               final IndexEntry indexEntry = index.findExact(selectedText);\r
628               if (indexEntry != null) {\r
629                 final TokenRow tokenRow = index.rows.get(indexEntry.startRow).getTokenRow(false);\r
630                 if (tokenRow != null && tokenRow.hasMainEntry) {\r
631                   indexToUse = i;\r
632                   break;\r
633                 }\r
634               }\r
635             } else {\r
636               Log.w(LOG, "Skipping findExact on index " + index.shortName);\r
637             }\r
638           }\r
639           if (indexToUse == -1) {\r
640             indexToUse = selectedSpannableIndex;\r
641           }\r
642           final boolean changeIndex = indexIndex != indexToUse;\r
643           setSearchText(selectedText, !changeIndex);  // If we're not changing index, we have to triggerSearch.\r
644           if (changeIndex) {\r
645             changeIndexGetFocusAndResearch(indexToUse);\r
646           }\r
647           // Give focus back to list view because typing is done.\r
648           getListView().requestFocus();\r
649           return false;\r
650         }\r
651       });\r
652     }\r
653     \r
654 \r
655   }\r
656   \r
657   @Override\r
658   protected void onListItemClick(ListView l, View v, int row, long id) {\r
659     defocusSearchText();\r
660     if (clickOpensContextMenu && dictRaf != null) {\r
661       openContextMenu(v);\r
662     }\r
663   }\r
664   \r
665   void onAppendToWordList(final RowBase row) {\r
666     defocusSearchText();\r
667     \r
668     final StringBuilder rawText = new StringBuilder();\r
669     rawText.append(\r
670         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))\r
671         .append("\t");\r
672     rawText.append(index.longName).append("\t");\r
673     rawText.append(row.getTokenRow(true).getToken()).append("\t");\r
674     rawText.append(row.getRawText(saveOnlyFirstSubentry));\r
675     Log.d(LOG, "Writing : " + rawText);\r
676 \r
677     try {\r
678       wordList.getParentFile().mkdirs();\r
679       final PrintWriter out = new PrintWriter(\r
680           new FileWriter(wordList, true));\r
681       out.println(rawText.toString());\r
682       out.close();\r
683     } catch (IOException e) {\r
684       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);\r
685       Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);\r
686     }\r
687     return;\r
688   }\r
689   \r
690   /**\r
691    * Called when user clicks outside of search text, so that they can start\r
692    * typing again immediately.\r
693    */\r
694   void defocusSearchText() {\r
695     //Log.d(LOG, "defocusSearchText");\r
696     // Request focus so that if we start typing again, it clears the text input.\r
697     getListView().requestFocus();\r
698     \r
699     // Visual indication that a new keystroke will clear the search text.\r
700     searchText.selectAll();\r
701   }\r
702 \r
703   void onCopy(final RowBase row) {\r
704     defocusSearchText();\r
705 \r
706     Log.d(LOG, "Copy, row=" + row);\r
707     final StringBuilder result = new StringBuilder();\r
708     result.append(row.getRawText(false));\r
709     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\r
710     clipboardManager.setText(result.toString());\r
711     Log.d(LOG, "Copied: " + result);\r
712   }\r
713 \r
714   @Override\r
715   public boolean onKeyDown(final int keyCode, final KeyEvent event) {\r
716     if (event.getUnicodeChar() != 0) {\r
717       if (!searchText.hasFocus()) {\r
718         setSearchText("" + (char) event.getUnicodeChar(), true);\r
719       }\r
720       return true;\r
721     }\r
722     if (keyCode == KeyEvent.KEYCODE_BACK) {\r
723       Log.d(LOG, "Clearing dictionary prefs.");\r
724       // Pretend that we just autolaunched so that we won't do it again.\r
725       DictionaryManagerActivity.lastAutoLaunchMillis = System.currentTimeMillis();\r
726     }\r
727     if (keyCode == KeyEvent.KEYCODE_ENTER) {\r
728       Log.d(LOG, "Trying to hide soft keyboard.");\r
729       final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);\r
730       inputManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);\r
731       return true;\r
732     }\r
733     return super.onKeyDown(keyCode, event);\r
734   }\r
735 \r
736   private void setSearchText(final String text, final boolean triggerSearch) {\r
737     if (!triggerSearch) {\r
738       getListView().requestFocus();\r
739     }\r
740     searchText.setText(text);\r
741     searchText.requestFocus();\r
742     if (searchText.getLayout() != null) {\r
743       // Surprising, but this can crash when you rotate...\r
744       Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());\r
745     }\r
746     if (triggerSearch) {\r
747       onSearchTextChange(text);\r
748     }\r
749   }\r
750 \r
751 \r
752   // --------------------------------------------------------------------------\r
753   // SearchOperation\r
754   // --------------------------------------------------------------------------\r
755 \r
756   private void searchFinished(final SearchOperation searchOperation) {\r
757     if (searchOperation.interrupted.get()) {\r
758       Log.d(LOG, "Search operation was interrupted: " + searchOperation);\r
759       return;\r
760     }\r
761     if (searchOperation != this.currentSearchOperation) {\r
762       Log.d(LOG, "Stale searchOperation finished: " + searchOperation);\r
763       return;\r
764     }\r
765     \r
766     final Index.IndexEntry searchResult = searchOperation.searchResult;\r
767     Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult);\r
768 \r
769     currentSearchOperation = null;\r
770     uiHandler.postDelayed(new Runnable() {\r
771       @Override\r
772       public void run() {\r
773         if (currentSearchOperation == null) {\r
774           if (searchResult != null) {\r
775             if (isFiltered()) {\r
776               clearFiltered();\r
777             }\r
778             jumpToRow(searchResult.startRow);\r
779           } else if (searchOperation.multiWordSearchResult != null) {\r
780             // Multi-row search....\r
781             setFiltered(searchOperation);\r
782           } else {\r
783             throw new IllegalStateException("This should never happen.");\r
784           }\r
785         } else {\r
786           Log.d(LOG, "More coming, waiting for currentSearchOperation.");\r
787         }\r
788       }\r
789     }, 20);\r
790     \r
791   }\r
792   \r
793   private final void jumpToRow(final int row) {\r
794     setSelection(row);\r
795     getListView().setSelected(true);\r
796   }\r
797 \r
798   static final Pattern WHITESPACE = Pattern.compile("\\s+");\r
799   final class SearchOperation implements Runnable {\r
800     \r
801     final AtomicBoolean interrupted = new AtomicBoolean(false);\r
802     final String searchText;\r
803     List<String> searchTokens;  // filled in for multiWord.\r
804     final Index index;\r
805     \r
806     long searchStartMillis;\r
807 \r
808     Index.IndexEntry searchResult;\r
809     List<RowBase> multiWordSearchResult;\r
810     \r
811     boolean done = false;\r
812     \r
813     SearchOperation(final String searchText, final Index index) {\r
814       this.searchText = searchText.trim();\r
815       this.index = index;\r
816     }\r
817     \r
818     public String toString() {\r
819       return String.format("SearchOperation(%s,%s)", searchText, interrupted.toString());\r
820     }\r
821 \r
822     @Override\r
823     public void run() {\r
824       try {\r
825         searchStartMillis = System.currentTimeMillis();\r
826         final String[] searchTokenArray = WHITESPACE.split(searchText);\r
827         if (searchTokenArray.length == 1) {\r
828           searchResult = index.findInsertionPoint(searchText, interrupted);\r
829         } else {\r
830           searchTokens = Arrays.asList(searchTokenArray);\r
831           multiWordSearchResult = index.multiWordSearch(searchTokens, interrupted);\r
832         }\r
833         Log.d(LOG, "searchText=" + searchText + ", searchDuration="\r
834             + (System.currentTimeMillis() - searchStartMillis) + ", interrupted="\r
835             + interrupted.get());\r
836         if (!interrupted.get()) {\r
837           uiHandler.post(new Runnable() {\r
838             @Override\r
839             public void run() {            \r
840               searchFinished(SearchOperation.this);\r
841             }\r
842           });\r
843         }\r
844       } catch (Exception e) {\r
845         Log.e(LOG, "Failure during search (can happen during Activity close.");\r
846       } finally {\r
847         synchronized (this) {\r
848           done = true;\r
849           this.notifyAll();\r
850         }\r
851       }\r
852     }\r
853   }\r
854 \r
855   \r
856   // --------------------------------------------------------------------------\r
857   // IndexAdapter\r
858   // --------------------------------------------------------------------------\r
859 \r
860   final class IndexAdapter extends BaseAdapter {\r
861     \r
862     final Index index;\r
863     final List<RowBase> rows;\r
864     final Set<String> toHighlight;\r
865 \r
866     IndexAdapter(final Index index) {\r
867       this.index = index;\r
868       rows = index.rows;\r
869       this.toHighlight = null;\r
870     }\r
871 \r
872     IndexAdapter(final Index index, final List<RowBase> rows, final List<String> toHighlight) {\r
873       this.index = index;\r
874       this.rows = rows;\r
875       this.toHighlight = new LinkedHashSet<String>(toHighlight);\r
876     }\r
877 \r
878     @Override\r
879     public int getCount() {\r
880       return rows.size();\r
881     }\r
882 \r
883     @Override\r
884     public RowBase getItem(int position) {\r
885       return rows.get(position);\r
886     }\r
887 \r
888     @Override\r
889     public long getItemId(int position) {\r
890       return getItem(position).index();\r
891     }\r
892 \r
893     @Override\r
894     public TableLayout getView(int position, View convertView, ViewGroup parent) {\r
895       final TableLayout result;\r
896       if (convertView instanceof TableLayout) {\r
897         result = (TableLayout) convertView;\r
898         result.removeAllViews();\r
899       } else {\r
900         result = new TableLayout(parent.getContext());\r
901       }\r
902       final RowBase row = getItem(position);\r
903       if (row instanceof PairEntry.Row) {\r
904         return getView(position, (PairEntry.Row) row, parent, result);\r
905       } else if (row instanceof TokenRow) {\r
906         return getView((TokenRow) row, parent, result);\r
907       } else {\r
908         throw new IllegalArgumentException("Unsupported Row type: " + row.getClass());\r
909       }\r
910     }\r
911 \r
912     private TableLayout getView(final int position, PairEntry.Row row, ViewGroup parent, final TableLayout result) {\r
913       final PairEntry entry = row.getEntry();\r
914       final int rowCount = entry.pairs.size();\r
915       \r
916       final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();\r
917       layoutParams.weight = 0.5f;\r
918       \r
919       for (int r = 0; r < rowCount; ++r) {\r
920         final TableRow tableRow = new TableRow(result.getContext());\r
921 \r
922         final TextView col1 = new TextView(tableRow.getContext());\r
923         final TextView col2 = new TextView(tableRow.getContext());\r
924 \r
925         // Set the columns in the table.\r
926         if (r > 0) {\r
927           final TextView bullet = new TextView(tableRow.getContext());\r
928           bullet.setText(" • ");\r
929           tableRow.addView(bullet);\r
930         }\r
931         tableRow.addView(col1, layoutParams);\r
932         final TextView margin = new TextView(tableRow.getContext());\r
933         margin.setText(" ");\r
934         tableRow.addView(margin);\r
935         if (r > 0) {\r
936           final TextView bullet = new TextView(tableRow.getContext());\r
937           bullet.setText(" • ");\r
938           tableRow.addView(bullet);\r
939         }\r
940         tableRow.addView(col2, layoutParams);\r
941         col1.setWidth(1);\r
942         col2.setWidth(1);\r
943         \r
944         // Set what's in the columns.\r
945 \r
946         final Pair pair = entry.pairs.get(r);\r
947         final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1;\r
948         final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;\r
949         \r
950         col1.setText(col1Text, TextView.BufferType.SPANNABLE);\r
951         col2.setText(col2Text, TextView.BufferType.SPANNABLE);\r
952         \r
953         // Bold the token instances in col1.\r
954         final Set<String> toBold = toHighlight != null ? this.toHighlight : Collections.singleton(row.getTokenRow(true).getToken());\r
955         final Spannable col1Spannable = (Spannable) col1.getText();\r
956         for (final String token : toBold) {\r
957           int startPos = 0;\r
958           while ((startPos = col1Text.indexOf(token, startPos)) != -1) {\r
959             col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,\r
960                 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
961             startPos += token.length();\r
962           }\r
963         }\r
964         \r
965         createTokenLinkSpans(col1, col1Spannable, col1Text);\r
966         createTokenLinkSpans(col2, (Spannable) col2.getText(), col2Text);\r
967         \r
968         col1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
969         col2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);\r
970         // col2.setBackgroundResource(theme.otherLangBg);\r
971         \r
972         if (index.swapPairEntries) {\r
973           col2.setOnLongClickListener(textViewLongClickListenerIndex0);\r
974           col1.setOnLongClickListener(textViewLongClickListenerIndex1);\r
975         } else {\r
976           col1.setOnLongClickListener(textViewLongClickListenerIndex0);\r
977           col2.setOnLongClickListener(textViewLongClickListenerIndex1);\r
978         }\r
979         \r
980         result.addView(tableRow);\r
981       }\r
982 \r
983       // Because we have a Button inside a ListView row:\r
984       // http://groups.google.com/group/android-developers/browse_thread/thread/3d96af1530a7d62a?pli=1\r
985       result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);\r
986       result.setClickable(true);\r
987       result.setFocusable(true);\r
988       result.setLongClickable(true);\r
989       result.setBackgroundResource(android.R.drawable.menuitem_background);\r
990       result.setOnClickListener(new TextView.OnClickListener() {\r
991         @Override\r
992         public void onClick(View v) {\r
993           DictionaryActivity.this.onListItemClick(getListView(), v, position, position);\r
994         }\r
995       });\r
996 \r
997       return result;\r
998     }\r
999 \r
1000     private TableLayout getView(TokenRow row, ViewGroup parent, final TableLayout result) {\r
1001       final Context context = parent.getContext();\r
1002       final TextView textView = new TextView(context);\r
1003       textView.setText(row.getToken());\r
1004       // Doesn't work:\r
1005       //textView.setTextColor(android.R.color.secondary_text_light);\r
1006       textView.setTextAppearance(context, theme.tokenRowFg);\r
1007       textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 5 * fontSizeSp / 4);\r
1008       \r
1009       final TableRow tableRow = new TableRow(result.getContext());\r
1010       tableRow.addView(textView);\r
1011       tableRow.setBackgroundResource(row.hasMainEntry ? theme.tokenRowMainBg : theme.tokenRowOtherBg);\r
1012       result.addView(tableRow);\r
1013       return result;\r
1014     }\r
1015     \r
1016   }\r
1017 \r
1018   static final Pattern CHAR_DASH = Pattern.compile("['\\p{L}0-9]+");\r
1019 \r
1020   private void createTokenLinkSpans(final TextView textView, final Spannable spannable, final String text) {\r
1021     // Saw from the source code that LinkMovementMethod sets the selection!\r
1022     // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.1_r1/android/text/method/LinkMovementMethod.java#LinkMovementMethod\r
1023     textView.setMovementMethod(LinkMovementMethod.getInstance());\r
1024     final Matcher matcher = CHAR_DASH.matcher(text);\r
1025     while (matcher.find()) {\r
1026       spannable.setSpan(new NonLinkClickableSpan(), matcher.start(), matcher.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);\r
1027     }\r
1028   }\r
1029   \r
1030 \r
1031   String selectedSpannableText = null;\r
1032   int selectedSpannableIndex = -1;\r
1033 \r
1034   @Override\r
1035   public boolean onTouchEvent(MotionEvent event) {\r
1036     selectedSpannableText = null;\r
1037     selectedSpannableIndex = -1;\r
1038     return super.onTouchEvent(event);\r
1039   }\r
1040 \r
1041   private class TextViewLongClickListener implements OnLongClickListener {\r
1042     final int index;\r
1043     \r
1044     private TextViewLongClickListener(final int index) {\r
1045       this.index = index;\r
1046     }\r
1047 \r
1048     @Override\r
1049     public boolean onLongClick(final View v) {\r
1050       final TextView textView = (TextView) v;\r
1051       final int start = textView.getSelectionStart();\r
1052       final int end = textView.getSelectionEnd();\r
1053       if (start >= 0 &&  end >= 0) {\r
1054         selectedSpannableText = textView.getText().subSequence(start, end).toString();\r
1055         selectedSpannableIndex = index;\r
1056       }\r
1057       return false;\r
1058     }\r
1059   }\r
1060   final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(0);\r
1061   final TextViewLongClickListener textViewLongClickListenerIndex1 = new TextViewLongClickListener(1);\r
1062   \r
1063 \r
1064   // --------------------------------------------------------------------------\r
1065   // SearchText\r
1066   // --------------------------------------------------------------------------\r
1067 \r
1068   void onSearchTextChange(final String text) {\r
1069     if ("thadolina".equals(text)) {\r
1070       final Dialog dialog = new Dialog(getListView().getContext());\r
1071       dialog.setContentView(R.layout.thadolina_dialog);\r
1072       dialog.setTitle("Ti amo, amore mio!");\r
1073       final ImageView imageView = (ImageView) dialog.findViewById(R.id.thadolina_image);\r
1074       imageView.setOnClickListener(new OnClickListener() {\r
1075         @Override\r
1076         public void onClick(View v) {\r
1077           final Intent intent = new Intent(Intent.ACTION_VIEW);\r
1078           intent.setData(Uri.parse("https://sites.google.com/site/cfoxroxvday/vday2012"));\r
1079           startActivity(intent);\r
1080         }\r
1081       });\r
1082       dialog.show();\r
1083     }\r
1084     if (dictRaf == null) {\r
1085       Log.d(LOG, "searchText changed during shutdown, doing nothing.");\r
1086       return;\r
1087     }\r
1088     if (!searchText.isFocused()) {\r
1089       Log.d(LOG, "searchText changed without focus, doing nothing.");\r
1090       return;\r
1091     }\r
1092     Log.d(LOG, "onSearchTextChange: " + text);    \r
1093     if (currentSearchOperation != null) {\r
1094       Log.d(LOG, "Interrupting currentSearchOperation.");\r
1095       currentSearchOperation.interrupted.set(true);\r
1096     }\r
1097     currentSearchOperation = new SearchOperation(text, index);\r
1098     searchExecutor.execute(currentSearchOperation);\r
1099   }\r
1100   \r
1101   private class SearchTextWatcher implements TextWatcher {\r
1102     public void afterTextChanged(final Editable searchTextEditable) {\r
1103       if (searchText.hasFocus()) {\r
1104         Log.d(LOG, "Search text changed with focus: " + searchText.getText());\r
1105         // If they were typing to cause the change, update the UI.\r
1106         onSearchTextChange(searchText.getText().toString());\r
1107       }\r
1108     }\r
1109 \r
1110     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,\r
1111         int arg3) {\r
1112     }\r
1113 \r
1114     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {\r
1115     }\r
1116   }\r
1117 \r
1118   // --------------------------------------------------------------------------\r
1119   // Filtered results.\r
1120   // --------------------------------------------------------------------------\r
1121 \r
1122   boolean isFiltered() {\r
1123     return rowsToShow != null;\r
1124   }\r
1125 \r
1126   void setFiltered(final SearchOperation searchOperation) {\r
1127     ((Button) findViewById(R.id.UpButton)).setEnabled(false);\r
1128     ((Button) findViewById(R.id.DownButton)).setEnabled(false);\r
1129     rowsToShow = searchOperation.multiWordSearchResult;\r
1130     setListAdapter(new IndexAdapter(index, rowsToShow, searchOperation.searchTokens));\r
1131   }\r
1132 \r
1133   void clearFiltered() {\r
1134     ((Button) findViewById(R.id.UpButton)).setEnabled(true);\r
1135     ((Button) findViewById(R.id.DownButton)).setEnabled(true);\r
1136     setListAdapter(new IndexAdapter(index));\r
1137     rowsToShow = null;\r
1138   }\r
1139 \r
1140 }\r