]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
Added intent framework
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 // Some Parts Copyright 2013 Dominik Köppl
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package com.hughes.android.dictionary;
16
17 import java.io.File;
18 import java.io.FileWriter;
19 import java.io.IOException;
20 import java.io.PrintWriter;
21 import java.io.RandomAccessFile;
22 import java.text.SimpleDateFormat;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.LinkedHashSet;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Random;
31 import java.util.Set;
32 import java.util.concurrent.Executor;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.ThreadFactory;
35 import java.util.concurrent.atomic.AtomicBoolean;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38
39 import android.app.AlertDialog;
40 import android.app.Dialog;
41 import android.app.ListActivity;
42 import android.app.SearchManager;
43 import android.content.Context;
44 import android.content.DialogInterface;
45 import android.content.Intent;
46 import android.content.SharedPreferences;
47 import android.graphics.Color;
48 import android.graphics.Typeface;
49 import android.net.Uri;
50 import android.os.Bundle;
51 import android.os.Handler;
52 import android.preference.PreferenceManager;
53 import android.speech.tts.TextToSpeech;
54 import android.speech.tts.TextToSpeech.OnInitListener;
55 import android.text.ClipboardManager;
56 import android.text.Editable;
57 import android.text.Selection;
58 import android.text.Spannable;
59 import android.text.TextWatcher;
60 import android.text.method.LinkMovementMethod;
61 import android.text.style.ClickableSpan;
62 import android.text.style.StyleSpan;
63 import android.util.Log;
64 import android.util.TypedValue;
65 import android.view.ContextMenu;
66 import android.view.ContextMenu.ContextMenuInfo;
67 import android.view.KeyEvent;
68 import android.view.Menu;
69 import android.view.MenuItem;
70 import android.view.MenuItem.OnMenuItemClickListener;
71 import android.view.MotionEvent;
72 import android.view.View;
73 import android.view.View.OnClickListener;
74 import android.view.View.OnFocusChangeListener;
75 import android.view.View.OnLongClickListener;
76 import android.view.ViewGroup;
77 import android.view.WindowManager;
78 import android.view.inputmethod.InputMethodManager;
79 import android.widget.AdapterView.AdapterContextMenuInfo;
80 import android.widget.BaseAdapter;
81 import android.widget.Button;
82 import android.widget.EditText;
83 import android.widget.ImageButton;
84 import android.widget.ImageView;
85 import android.widget.LinearLayout;
86 import android.widget.ListAdapter;
87 import android.widget.ListView;
88 import android.widget.TableLayout;
89 import android.widget.TableRow;
90 import android.widget.TextView;
91 import android.widget.TextView.BufferType;
92 import android.widget.Toast;
93
94 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;
95 import com.hughes.android.dictionary.engine.Dictionary;
96 import com.hughes.android.dictionary.engine.EntrySource;
97 import com.hughes.android.dictionary.engine.HtmlEntry;
98 import com.hughes.android.dictionary.engine.Index;
99 import com.hughes.android.dictionary.engine.Index.IndexEntry;
100 import com.hughes.android.dictionary.engine.PairEntry;
101 import com.hughes.android.dictionary.engine.PairEntry.Pair;
102 import com.hughes.android.dictionary.engine.RowBase;
103 import com.hughes.android.dictionary.engine.TokenRow;
104 import com.hughes.android.dictionary.engine.TransliteratorManager;
105 import com.hughes.android.util.IntentLauncher;
106 import com.hughes.android.util.NonLinkClickableSpan;
107 import com.hughes.util.StringUtil;
108
109 public class DictionaryActivity extends ListActivity {
110
111     static final String LOG = "QuickDic";
112
113     DictionaryApplication application;
114
115     File dictFile = null;
116
117     RandomAccessFile dictRaf = null;
118
119     Dictionary dictionary = null;
120
121     int indexIndex = 0;
122
123     Index index = null;
124
125     List<RowBase> rowsToShow = null; // if not null, just show these rows.
126
127     // package for test.
128     final Handler uiHandler = new Handler();
129
130     TextToSpeech textToSpeech;
131     volatile boolean ttsReady;
132     
133     int textColorFg = Color.BLACK;
134     
135
136     private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
137         @Override
138         public Thread newThread(Runnable r) {
139             return new Thread(r, "searchExecutor");
140         }
141     });
142
143     private SearchOperation currentSearchOperation = null;
144
145     C.Theme theme = C.Theme.LIGHT;
146
147     Typeface typeface;
148
149     int fontSizeSp;
150
151     EditText searchText;
152
153     Button langButton;
154
155     // Never null.
156     private File wordList = null;
157
158     private boolean saveOnlyFirstSubentry = false;
159
160     private boolean clickOpensContextMenu = false;
161
162     // Visible for testing.
163     ListAdapter indexAdapter = null;
164
165     final SearchTextWatcher searchTextWatcher = new SearchTextWatcher();
166
167     /**
168      * For some languages, loading the transliterators used in this search takes
169      * a long time, so we fire it up on a different thread, and don't invoke it
170      * from the main thread until it's already finished once.
171      */
172     private volatile boolean indexPrepFinished = false;
173
174     public DictionaryActivity() {
175     }
176
177     public static Intent getLaunchIntent(final File dictFile, final int indexIndex,
178             final String searchToken) {
179         final Intent intent = new Intent();
180         intent.setClassName(DictionaryActivity.class.getPackage().getName(),
181                 DictionaryActivity.class.getName());
182         intent.putExtra(C.DICT_FILE, dictFile.getPath());
183         intent.putExtra(C.INDEX_INDEX, indexIndex);
184         intent.putExtra(C.SEARCH_TOKEN, searchToken);
185         return intent;
186     }
187
188     @Override
189     protected void onSaveInstanceState(final Bundle outState) {
190         super.onSaveInstanceState(outState);
191         Log.d(LOG, "onSaveInstanceState: " + searchText.getText().toString());
192         outState.putInt(C.INDEX_INDEX, indexIndex);
193         outState.putString(C.SEARCH_TOKEN, searchText.getText().toString());
194     }
195
196     @Override
197     protected void onRestoreInstanceState(final Bundle outState) {
198         super.onRestoreInstanceState(outState);
199         Log.d(LOG, "onRestoreInstanceState: " + outState.getString(C.SEARCH_TOKEN));
200         onCreate(outState);
201     }
202
203     @Override
204     public void onCreate(Bundle savedInstanceState) {
205         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
206         prefs.edit().remove(C.INDEX_INDEX).commit(); // Don't auto-launch if
207                                                      // this fails.
208
209         setTheme(((DictionaryApplication) getApplication()).getSelectedTheme().themeId);
210
211         Log.d(LOG, "onCreate:" + this);
212         super.onCreate(savedInstanceState);
213
214         application = (DictionaryApplication) getApplication();
215         theme = application.getSelectedTheme();
216         textColorFg = getResources().getColor(theme.tokenRowFgColor);
217
218         
219
220         final Intent intent = getIntent();
221         String intentAction = intent.getAction();
222         /**
223          * @author Dominik Köppl
224          * Querying the Intent
225          * com.hughes.action.ACTION_SEARCH_DICT is the advanced query
226          * Arguments:
227          * SearchManager.QUERY -> the phrase to search
228          * from -> language in which the phrase is written
229          * to -> to which language shall be translated
230          */
231         if(intentAction != null && intentAction.equals("com.hughes.action.ACTION_SEARCH_DICT")) 
232         {
233                 String query = intent.getStringExtra(SearchManager.QUERY);
234                 String from = intent.getStringExtra("from");
235                 if(from != null) from = from.toLowerCase(Locale.US);
236                 String to = intent.getStringExtra("to");
237                 if(to != null) to = to.toLowerCase(Locale.US);
238                 if(query != null)
239                 {
240                         getIntent().putExtra(C.SEARCH_TOKEN, query);
241                 }
242                 if(intent.getStringExtra(C.DICT_FILE) == null && (from != null || to != null))
243                 {
244                          Log.d(LOG, "DictSearch: from: " + from + " to " + to);
245                         List<DictionaryInfo> dicts = application.getUsableDicts();
246                         for(DictionaryInfo info : dicts)
247                         {
248                                 boolean hasFrom = from == null;
249                                 boolean hasTo = to == null;
250                                 for(IndexInfo index : info.indexInfos)
251                                 {
252                                         if(!hasFrom && index.shortName.toLowerCase(Locale.US).equals(from)) hasFrom = true;
253                                         if(!hasTo && index.shortName.toLowerCase(Locale.US).equals(to)) hasTo = true;
254                                 }
255                                 if(hasFrom && hasTo)
256                                 {
257                                         if(from != null)
258                                         {
259                                                 int which_index = 0;
260                                         for(;which_index < info.indexInfos.size(); ++which_index)
261                                         {
262                                                 if(info.indexInfos.get(which_index).shortName.toLowerCase(Locale.US).equals(from))
263                                                         break;
264                                         }
265                                         intent.putExtra(C.INDEX_INDEX, which_index);
266                                         
267                                         }
268                                         intent.putExtra(C.DICT_FILE, application.getPath(info.uncompressedFilename).toString());
269                                         break;
270                                 }
271                         }
272                         
273                 }
274         }
275         /**
276          * @author Dominik Köppl
277          * Querying the Intent
278          * Intent.ACTION_SEARCH is a simple query
279          * Arguments follow from android standard (see documentation)
280          */
281         if (intentAction != null && intentAction.equals(Intent.ACTION_SEARCH)) 
282         {
283             String query = intent.getStringExtra(SearchManager.QUERY);
284                 if(query != null) getIntent().putExtra(C.SEARCH_TOKEN,query);
285         }
286         /**
287          * @author Dominik Köppl
288          * If no dictionary is chosen, use the default dictionary specified in the preferences
289          * If this step does fail (no default directory specified), show a toast and abort.
290          */
291         if(intent.getStringExtra(C.DICT_FILE) == null)
292         {
293                 String dictfile = prefs.getString(getString(R.string.defaultDicKey), null);
294                 if(dictfile != null) intent.putExtra(C.DICT_FILE, application.getPath(dictfile).toString());
295         }
296         String dictFilename = intent.getStringExtra(C.DICT_FILE);
297         
298         if(dictFilename == null)
299         {
300             Toast.makeText(this, getString(R.string.no_dict_file), Toast.LENGTH_LONG).show();
301             startActivity(DictionaryManagerActivity.getLaunchIntent());
302             finish();
303             return;
304         }
305         if(dictFilename != null) dictFile = new File(dictFilename);
306
307         ttsReady = false;
308         textToSpeech = new TextToSpeech(getApplicationContext(), new OnInitListener() {
309             @Override
310             public void onInit(int status) {
311                 ttsReady = true;
312                 updateTTSLanguage();
313             }
314         });
315         
316         try {
317             final String name = application.getDictionaryName(dictFile.getName());
318             this.setTitle("QuickDic: " + name);
319             dictRaf = new RandomAccessFile(dictFile, "r");
320             dictionary = new Dictionary(dictRaf);
321         } catch (Exception e) {
322             Log.e(LOG, "Unable to load dictionary.", e);
323             if (dictRaf != null) {
324                 try {
325                     dictRaf.close();
326                 } catch (IOException e1) {
327                     Log.e(LOG, "Unable to close dictRaf.", e1);
328                 }
329                 dictRaf = null;
330             }
331             Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()),
332                     Toast.LENGTH_LONG).show();
333             startActivity(DictionaryManagerActivity.getLaunchIntent());
334             finish();
335             return;
336         }
337         indexIndex = intent.getIntExtra(C.INDEX_INDEX, 0);
338         if (savedInstanceState != null) {
339             indexIndex = savedInstanceState.getInt(C.INDEX_INDEX, indexIndex);
340         }
341         indexIndex %= dictionary.indices.size();
342         Log.d(LOG, "Loading index " + indexIndex);
343         index = dictionary.indices.get(indexIndex);
344         setListAdapter(new IndexAdapter(index));
345
346         // Pre-load the collators.
347         new Thread(new Runnable() {
348             public void run() {
349                 final long startMillis = System.currentTimeMillis();
350                 try {
351                     TransliteratorManager.init(new TransliteratorManager.Callback() {
352                         @Override
353                         public void onTransliteratorReady() {
354                             uiHandler.post(new Runnable() {
355                                 @Override
356                                 public void run() {
357                                     onSearchTextChange(searchText.getText().toString());
358                                 }
359                             });
360                         }
361                     });
362
363                     for (final Index index : dictionary.indices) {
364                         final String searchToken = index.sortedIndexEntries.get(0).token;
365                         final IndexEntry entry = index.findExact(searchToken);
366                         if (!searchToken.equals(entry.token)) {
367                             Log.e(LOG, "Couldn't find token: " + searchToken + ", " + entry.token);
368                         }
369                     }
370                     indexPrepFinished = true;
371                 } catch (Exception e) {
372                     Log.w(LOG,
373                             "Exception while prepping.  This can happen if dictionary is closed while search is happening.");
374                 }
375                 Log.d(LOG, "Prepping indices took:" + (System.currentTimeMillis() - startMillis));
376             }
377         }).start();
378
379         String fontName = prefs.getString(getString(R.string.fontKey), "FreeSerif.ttf.jpg");
380         if ("SYSTEM".equals(fontName)) {
381             typeface = Typeface.DEFAULT;
382         } else {
383             try {
384                 typeface = Typeface.createFromAsset(getAssets(), fontName);
385             } catch (Exception e) {
386                 Log.w(LOG, "Exception trying to use typeface, using default.", e);
387                 Toast.makeText(this, getString(R.string.fontFailure, e.getLocalizedMessage()),
388                         Toast.LENGTH_LONG).show();
389             }
390         }
391         // if (!"SYSTEM".equals(fontName)) {
392         // throw new RuntimeException("Test force using system font: " +
393         // fontName);
394         // }
395         if (typeface == null) {
396             Log.w(LOG, "Unable to create typeface, using default.");
397             typeface = Typeface.DEFAULT;
398         }
399         final String fontSize = prefs.getString(getString(R.string.fontSizeKey), "14");
400         try {
401             fontSizeSp = Integer.parseInt(fontSize.trim());
402         } catch (NumberFormatException e) {
403             fontSizeSp = 14;
404         }
405
406         setContentView(R.layout.dictionary_activity);
407         searchText = (EditText) findViewById(R.id.SearchText);
408         searchText.requestFocus();
409         searchText.addTextChangedListener(searchTextWatcher);
410         searchText.setOnFocusChangeListener(new OnFocusChangeListener() {
411             @Override
412             public void onFocusChange(View v, boolean hasFocus) {
413                 Log.d(LOG, "searchText onFocusChange hasFocus=" + hasFocus);
414             }
415         });
416
417         // Set the search text from the intent, then the saved state.
418         String text = getIntent().getStringExtra(C.SEARCH_TOKEN);
419         if (savedInstanceState != null) {
420             text = savedInstanceState.getString(C.SEARCH_TOKEN);
421         }
422         if (text == null) {
423             text = "";
424         }
425         setSearchText(text, true);
426         Log.d(LOG, "Trying to restore searchText=" + text);
427
428         final View clearSearchTextButton = findViewById(R.id.ClearSearchTextButton);
429         clearSearchTextButton.setOnClickListener(new OnClickListener() {
430             public void onClick(View v) {
431                 onClearSearchTextButton();
432             }
433         });
434         clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this)
435                 .getBoolean(getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
436                 : View.GONE);
437
438         langButton = (Button) findViewById(R.id.LangButton);
439         langButton.setOnClickListener(new OnClickListener() {
440             public void onClick(View v) {
441                 onLanguageButton();
442             }
443         });
444         langButton.setOnLongClickListener(new OnLongClickListener() {
445             @Override
446             public boolean onLongClick(View v) {
447                 onLanguageButtonLongClick(v.getContext());
448                 return true;
449             }
450         });
451         updateLangButton();
452
453         final View upButton = findViewById(R.id.UpButton);
454         upButton.setOnClickListener(new OnClickListener() {
455             public void onClick(View v) {
456                 onUpDownButton(true);
457             }
458         });
459         final View downButton = findViewById(R.id.DownButton);
460         downButton.setOnClickListener(new OnClickListener() {
461             public void onClick(View v) {
462                 onUpDownButton(false);
463             }
464         });
465         upButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this)
466                 .getBoolean(getString(R.string.showPrevNextButtonsKey), true) ? View.VISIBLE
467                 : View.GONE);
468         downButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this)
469                 .getBoolean(getString(R.string.showPrevNextButtonsKey), true) ? View.VISIBLE
470                 : View.GONE);
471
472 //        getListView().setOnItemSelectedListener(new ListView.OnItemSelectedListener() {
473 //            @Override
474 //            public void onItemSelected(AdapterView<?> adapterView, View arg1, final int position,
475 //                    long id) {
476 //                if (!searchText.isFocused()) {
477 //                    if (!isFiltered()) {
478 //                        final RowBase row = (RowBase) getListAdapter().getItem(position);
479 //                        Log.d(LOG, "onItemSelected: " + row.index());
480 //                        final TokenRow tokenRow = row.getTokenRow(true);
481 //                        searchText.setText(tokenRow.getToken());
482 //                    }
483 //                }
484 //            }
485 //
486 //            @Override
487 //            public void onNothingSelected(AdapterView<?> arg0) {
488 //            }
489 //        });
490 //
491         // ContextMenu.
492         registerForContextMenu(getListView());
493
494         // Prefs.
495         wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
496                 getString(R.string.wordListFileDefault)));
497         saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey),
498                 false);
499         clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey),
500                 false);
501         // if (prefs.getBoolean(getString(R.string.vibrateOnFailedSearchKey),
502         // true)) {
503         // vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
504         // }
505         Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);
506
507         setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());
508         
509
510         
511     }
512
513     @Override
514     protected void onResume() {
515         Log.d(LOG, "onResume");
516         super.onResume();
517         if (PreferenceActivity.prefsMightHaveChanged) {
518             PreferenceActivity.prefsMightHaveChanged = false;
519             finish();
520             startActivity(getIntent());
521         }
522         showKeyboard();
523     }
524
525     @Override
526     protected void onPause() {
527         super.onPause();
528     }
529
530     @Override
531     protected void onActivityResult(int requestCode, int resultCode, Intent result) {
532         super.onActivityResult(requestCode, resultCode, result);
533         if (result != null && result.hasExtra(C.SEARCH_TOKEN)) {
534             Log.d(LOG, "onActivityResult: " + result.getStringExtra(C.SEARCH_TOKEN));
535             jumpToTextFromHyperLink(result.getStringExtra(C.SEARCH_TOKEN), indexIndex);
536         }
537     }
538
539     private static void setDictionaryPrefs(final Context context, final File dictFile,
540             final int indexIndex, final String searchToken) {
541         final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(
542                 context).edit();
543         prefs.putString(C.DICT_FILE, dictFile.getPath());
544         prefs.putInt(C.INDEX_INDEX, indexIndex);
545         prefs.putString(C.SEARCH_TOKEN, ""); // Don't need to save search token.
546         prefs.commit();
547     }
548
549     @Override
550     protected void onDestroy() {
551         super.onDestroy();
552         if (dictRaf == null) {
553             return;
554         }
555
556         final SearchOperation searchOperation = currentSearchOperation;
557         currentSearchOperation = null;
558
559         // Before we close the RAF, we have to wind the current search down.
560         if (searchOperation != null) {
561             Log.d(LOG, "Interrupting search to shut down.");
562             currentSearchOperation = null;
563             searchOperation.interrupted.set(true);
564         }
565
566         try {
567             Log.d(LOG, "Closing RAF.");
568             dictRaf.close();
569         } catch (IOException e) {
570             Log.e(LOG, "Failed to close dictionary", e);
571         }
572         dictRaf = null;
573     }
574
575     // --------------------------------------------------------------------------
576     // Buttons
577     // --------------------------------------------------------------------------
578
579     private void onClearSearchTextButton() {
580         setSearchText("", true);
581         showKeyboard();
582     }
583
584     private void showKeyboard() {
585         searchText.postDelayed(new Runnable() {
586             @Override
587             public void run() {
588                 Log.d(LOG, "Trying to show soft keyboard.");
589                 final boolean searchTextHadFocus = searchText.hasFocus();
590                 searchText.requestFocus();
591                 final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
592                 manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);
593                 if (!searchTextHadFocus) {
594                     defocusSearchText();
595                 }
596             }}, 100);
597     }
598
599     void updateLangButton() {
600         // final LanguageResources languageResources =
601         // Language.isoCodeToResources.get(index.shortName);
602         // if (languageResources != null && languageResources.flagId != 0) {
603         // langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0,
604         // languageResources.flagId, 0);
605         // } else {
606         // langButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
607         langButton.setText(index.shortName);
608         // }
609         updateTTSLanguage();
610     }
611
612     private void updateTTSLanguage() {
613         if (!ttsReady || index == null || textToSpeech == null) {
614             Log.d(LOG, "Can't updateTTSLanguage.");
615             return;
616         }
617         final Locale locale = new Locale(index.sortLanguage.getIsoCode());
618         Log.d(LOG, "Setting TTS locale to: " + locale);
619         final int ttsResult = textToSpeech.setLanguage(locale);
620         if (ttsResult != TextToSpeech.LANG_AVAILABLE || 
621             ttsResult != TextToSpeech.LANG_COUNTRY_AVAILABLE) {
622             Log.e(LOG, "TTS not available in this language: ttsResult=" + ttsResult);
623         }
624     }
625
626     void onLanguageButton() {
627         if (currentSearchOperation != null) {
628             currentSearchOperation.interrupted.set(true);
629             currentSearchOperation = null;
630         }
631         changeIndexAndResearch((indexIndex + 1) % dictionary.indices.size());
632     }
633
634     void onLanguageButtonLongClick(final Context context) {
635         final Dialog dialog = new Dialog(context);
636         dialog.setContentView(R.layout.select_dictionary_dialog);
637         dialog.setTitle(R.string.selectDictionary);
638
639         final List<DictionaryInfo> installedDicts = ((DictionaryApplication) getApplication())
640                 .getUsableDicts();
641
642         ListView listView = (ListView) dialog.findViewById(android.R.id.list);
643
644         // final LinearLayout.LayoutParams layoutParams = new
645         // LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
646         // ViewGroup.LayoutParams.WRAP_CONTENT);
647         // layoutParams.width = 0;
648         // layoutParams.weight = 1.0f;
649
650         final Button button = new Button(listView.getContext());
651         final String name = getString(R.string.dictionaryManager);
652         button.setText(name);
653         final IntentLauncher intentLauncher = new IntentLauncher(listView.getContext(),
654                 DictionaryManagerActivity.getLaunchIntent()) {
655             @Override
656             protected void onGo() {
657                 dialog.dismiss();
658                 DictionaryActivity.this.finish();
659             };
660         };
661         button.setOnClickListener(intentLauncher);
662         // button.setLayoutParams(layoutParams);
663         listView.addHeaderView(button);
664         // listView.setHeaderDividersEnabled(true);
665
666         listView.setAdapter(new BaseAdapter() {
667             @Override
668             public View getView(int position, View convertView, ViewGroup parent) {
669                 final DictionaryInfo dictionaryInfo = getItem(position);
670
671                 final LinearLayout result = new LinearLayout(parent.getContext());
672
673                 for (int i = 0; i < dictionaryInfo.indexInfos.size(); ++i) {
674                     if (i > 0) {
675                         final TextView dash = new TextView(parent.getContext());
676                         dash.setText("-");
677                         result.addView(dash);
678                     }
679
680                     final IndexInfo indexInfo = dictionaryInfo.indexInfos.get(i);
681                     final Button button = new Button(parent.getContext());
682                     button.setText(indexInfo.shortName);
683                     final IntentLauncher intentLauncher = new IntentLauncher(parent.getContext(),
684                             getLaunchIntent(
685                                     application.getPath(dictionaryInfo.uncompressedFilename),
686                                     i, searchText.getText().toString())) {
687                         @Override
688                         protected void onGo() {
689                             dialog.dismiss();
690                             DictionaryActivity.this.finish();
691                         };
692                     };
693                     button.setOnClickListener(intentLauncher);
694                     result.addView(button);
695
696                 }
697
698                 final TextView nameView = new TextView(parent.getContext());
699                 final String name = application
700                         .getDictionaryName(dictionaryInfo.uncompressedFilename);
701                 nameView.setText(name);
702                 final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
703                         ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
704                 layoutParams.width = 0;
705                 layoutParams.weight = 1.0f;
706                 nameView.setLayoutParams(layoutParams);
707                 result.addView(nameView);
708
709                 return result;
710             }
711
712             @Override
713             public long getItemId(int position) {
714                 return position;
715             }
716
717             @Override
718             public DictionaryInfo getItem(int position) {
719                 return installedDicts.get(position);
720             }
721
722             @Override
723             public int getCount() {
724                 return installedDicts.size();
725             }
726         });
727
728         dialog.show();
729     }
730
731     private void changeIndexAndResearch(int newIndex) {
732         Log.d(LOG, "Changing index to: " + newIndex);
733         if (newIndex == -1) {
734             Log.e(LOG, "Invalid index.");
735             newIndex = 0;
736         }
737         indexIndex = newIndex;
738         index = dictionary.indices.get(indexIndex);
739         indexAdapter = new IndexAdapter(index);
740         Log.d(LOG, "changingIndex, newLang=" + index.longName);
741         setDictionaryPrefs(this, dictFile, indexIndex, searchText.getText().toString());
742         setListAdapter(indexAdapter);
743         updateLangButton();
744         setSearchText(searchText.getText().toString(), true);
745     }
746
747     void onUpDownButton(final boolean up) {
748         if (isFiltered()) {
749             return;
750         }
751         final int firstVisibleRow = getListView().getFirstVisiblePosition();
752         final RowBase row = index.rows.get(firstVisibleRow);
753         final TokenRow tokenRow = row.getTokenRow(true);
754         final int destIndexEntry;
755         if (up) {
756             if (row != tokenRow) {
757                 destIndexEntry = tokenRow.referenceIndex;
758             } else {
759                 destIndexEntry = Math.max(tokenRow.referenceIndex - 1, 0);
760             }
761         } else {
762             // Down
763             destIndexEntry = Math.min(tokenRow.referenceIndex + 1, index.sortedIndexEntries.size());
764         }
765         final Index.IndexEntry dest = index.sortedIndexEntries.get(destIndexEntry);
766         Log.d(LOG, "onUpDownButton, destIndexEntry=" + dest.token);
767         setSearchText(dest.token, false);
768         jumpToRow(index.sortedIndexEntries.get(destIndexEntry).startRow);
769         defocusSearchText();
770     }
771
772     // --------------------------------------------------------------------------
773     // Options Menu
774     // --------------------------------------------------------------------------
775
776     final Random random = new Random();
777
778     @Override
779     public boolean onCreateOptionsMenu(final Menu menu) {
780         application.onCreateGlobalOptionsMenu(this, menu);
781
782 //        {
783 //            final MenuItem randomWord = menu.add(getString(R.string.randomWord));
784 //            randomWord.setOnMenuItemClickListener(new OnMenuItemClickListener() {
785 //                public boolean onMenuItemClick(final MenuItem menuItem) {
786 //                    final String word = index.sortedIndexEntries.get(random
787 //                            .nextInt(index.sortedIndexEntries.size())).token;
788 //                    setSearchText(word, true);
789 //                    return false;
790 //                }
791 //            });
792 //        }
793
794         {
795             final MenuItem dictionaryList = menu.add(getString(R.string.dictionaryManager));
796             dictionaryList.setOnMenuItemClickListener(new OnMenuItemClickListener() {
797                 public boolean onMenuItemClick(final MenuItem menuItem) {
798                     startActivity(DictionaryManagerActivity.getLaunchIntent());
799                     finish();
800                     return false;
801                 }
802             });
803         }
804
805         {
806             final MenuItem aboutDictionary = menu.add(getString(R.string.aboutDictionary));
807             aboutDictionary.setOnMenuItemClickListener(new OnMenuItemClickListener() {
808                 public boolean onMenuItemClick(final MenuItem menuItem) {
809                     final Context context = getListView().getContext();
810                     final Dialog dialog = new Dialog(context);
811                     dialog.setContentView(R.layout.about_dictionary_dialog);
812                     final TextView textView = (TextView) dialog.findViewById(R.id.text);
813
814                     final String name = application.getDictionaryName(dictFile.getName());
815                     dialog.setTitle(name);
816
817                     final StringBuilder builder = new StringBuilder();
818                     final DictionaryInfo dictionaryInfo = dictionary.getDictionaryInfo();
819                     dictionaryInfo.uncompressedBytes = dictFile.length();
820                     if (dictionaryInfo != null) {
821                         builder.append(dictionaryInfo.dictInfo).append("\n\n");
822                         builder.append(getString(R.string.dictionaryPath, dictFile.getPath()))
823                                 .append("\n");
824                         builder.append(
825                                 getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes))
826                                 .append("\n");
827                         builder.append(
828                                 getString(R.string.dictionaryCreationTime,
829                                         dictionaryInfo.creationMillis)).append("\n");
830                         for (final IndexInfo indexInfo : dictionaryInfo.indexInfos) {
831                             builder.append("\n");
832                             builder.append(getString(R.string.indexName, indexInfo.shortName))
833                                     .append("\n");
834                             builder.append(
835                                     getString(R.string.mainTokenCount, indexInfo.mainTokenCount))
836                                     .append("\n");
837                         }
838                         builder.append("\n");
839                         builder.append(getString(R.string.sources)).append("\n");
840                         for (final EntrySource source : dictionary.sources) {
841                             builder.append(
842                                     getString(R.string.sourceInfo, source.getName(),
843                                             source.getNumEntries())).append("\n");
844                         }
845                     }
846                     // } else {
847                     // builder.append(getString(R.string.invalidDictionary));
848                     // }
849                     textView.setText(builder.toString());
850
851                     dialog.show();
852                     final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
853                     layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
854                     layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
855                     dialog.getWindow().setAttributes(layoutParams);
856                     return false;
857                 }
858             });
859         }
860
861         return true;
862     }
863
864     // --------------------------------------------------------------------------
865     // Context Menu + clicks
866     // --------------------------------------------------------------------------
867
868     @Override
869     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
870         AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;
871         final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);
872
873         final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList,
874                 wordList.getName()));
875         addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
876             public boolean onMenuItemClick(MenuItem item) {
877                 onAppendToWordList(row);
878                 return false;
879             }
880         });
881
882         final MenuItem share = menu.add("Share");
883         share.setOnMenuItemClickListener(new OnMenuItemClickListener() {
884             public boolean onMenuItemClick(MenuItem item) {
885                 Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
886                 shareIntent.setType("text/plain");
887                 shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, row.getTokenRow(true).getToken());
888                 shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, row.getRawText(saveOnlyFirstSubentry));
889                 startActivity(shareIntent);
890                 return false;
891             }
892         });
893
894         final MenuItem copy = menu.add(android.R.string.copy);
895         copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
896             public boolean onMenuItemClick(MenuItem item) {
897                 onCopy(row);
898                 return false;
899             }
900         });
901
902         if (selectedSpannableText != null) {
903             final String selectedText = selectedSpannableText;
904             final MenuItem searchForSelection = menu.add(getString(R.string.searchForSelection,
905                     selectedSpannableText));
906             searchForSelection.setOnMenuItemClickListener(new OnMenuItemClickListener() {
907                 public boolean onMenuItemClick(MenuItem item) {
908                     jumpToTextFromHyperLink(selectedText, selectedSpannableIndex);
909                     return false;
910                 }
911             });
912         }
913
914         if (row instanceof TokenRow && ttsReady) {
915             final MenuItem speak = menu.add(R.string.speak);
916             speak.setOnMenuItemClickListener(new OnMenuItemClickListener() {
917                 @Override
918                 public boolean onMenuItemClick(MenuItem item) {
919                     textToSpeech.speak(((TokenRow) row).getToken(), TextToSpeech.QUEUE_FLUSH,
920                             new HashMap<String, String>());
921                     return false;
922                 }
923             });
924         }
925     }
926
927     private void jumpToTextFromHyperLink(final String selectedText, final int defaultIndexToUse) {
928         int indexToUse = -1;
929         for (int i = 0; i < dictionary.indices.size(); ++i) {
930             final Index index = dictionary.indices.get(i);
931             if (indexPrepFinished) {
932                 System.out.println("Doing index lookup: on " + selectedText);
933                 final IndexEntry indexEntry = index.findExact(selectedText);
934                 if (indexEntry != null) {
935                     final TokenRow tokenRow = index.rows.get(indexEntry.startRow)
936                             .getTokenRow(false);
937                     if (tokenRow != null && tokenRow.hasMainEntry) {
938                         indexToUse = i;
939                         break;
940                     }
941                 }
942             } else {
943                 Log.w(LOG, "Skipping findExact on index " + index.shortName);
944             }
945         }
946         if (indexToUse == -1) {
947             indexToUse = defaultIndexToUse;
948         }
949         final boolean changeIndex = indexIndex != indexToUse;
950         if (changeIndex) {
951             setSearchText(selectedText, false);
952             changeIndexAndResearch(indexToUse);
953         } else {
954             setSearchText(selectedText, true);
955         }
956     }
957
958     @Override
959     protected void onListItemClick(ListView l, View v, int row, long id) {
960         defocusSearchText();
961         if (clickOpensContextMenu && dictRaf != null) {
962             openContextMenu(v);
963         }
964     }
965
966     void onAppendToWordList(final RowBase row) {
967         defocusSearchText();
968
969         final StringBuilder rawText = new StringBuilder();
970         rawText.append(new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date())).append("\t");
971         rawText.append(index.longName).append("\t");
972         rawText.append(row.getTokenRow(true).getToken()).append("\t");
973         rawText.append(row.getRawText(saveOnlyFirstSubentry));
974         Log.d(LOG, "Writing : " + rawText);
975
976         try {
977             wordList.getParentFile().mkdirs();
978             final PrintWriter out = new PrintWriter(new FileWriter(wordList, true));
979             out.println(rawText.toString());
980             out.close();
981         } catch (IOException e) {
982             Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
983             Toast.makeText(this,
984                     getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()),
985                     Toast.LENGTH_LONG).show();
986         }
987         return;
988     }
989
990     /**
991      * Called when user clicks outside of search text, so that they can start
992      * typing again immediately.
993      */
994     void defocusSearchText() {
995         // Log.d(LOG, "defocusSearchText");
996         // Request focus so that if we start typing again, it clears the text
997         // input.
998         getListView().requestFocus();
999
1000         // Visual indication that a new keystroke will clear the search text.
1001         // Doesn't seem to work unless earchText has focus.
1002         // searchText.selectAll();
1003     }
1004
1005     @SuppressWarnings("deprecation")
1006     void onCopy(final RowBase row) {
1007         defocusSearchText();
1008
1009         Log.d(LOG, "Copy, row=" + row);
1010         final StringBuilder result = new StringBuilder();
1011         result.append(row.getRawText(false));
1012         final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
1013         clipboardManager.setText(result.toString());
1014         Log.d(LOG, "Copied: " + result);
1015     }
1016
1017     @Override
1018     public boolean onKeyDown(final int keyCode, final KeyEvent event) {
1019         if (event.getUnicodeChar() != 0) {
1020             if (!searchText.hasFocus()) {
1021                 setSearchText("" + (char) event.getUnicodeChar(), true);
1022             }
1023             return true;
1024         }
1025         if (keyCode == KeyEvent.KEYCODE_BACK) {
1026             // Log.d(LOG, "Clearing dictionary prefs.");
1027             // Pretend that we just autolaunched so that we won't do it again.
1028             // DictionaryManagerActivity.lastAutoLaunchMillis =
1029             // System.currentTimeMillis();
1030         }
1031         if (keyCode == KeyEvent.KEYCODE_ENTER) {
1032             Log.d(LOG, "Trying to hide soft keyboard.");
1033             final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
1034             inputManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(),
1035                     InputMethodManager.HIDE_NOT_ALWAYS);
1036             return true;
1037         }
1038         return super.onKeyDown(keyCode, event);
1039     }
1040
1041     private void setSearchText(final String text, final boolean triggerSearch) {
1042         if (!triggerSearch) {
1043             getListView().requestFocus();
1044         }
1045         searchText.setText(text);
1046         searchText.requestFocus();
1047         moveCursorToRight();
1048         if (triggerSearch) {
1049             onSearchTextChange(text);
1050         }
1051     }
1052
1053     private long cursorDelayMillis = 100;
1054
1055     private void moveCursorToRight() {
1056         if (searchText.getLayout() != null) {
1057             cursorDelayMillis = 100;
1058             // Surprising, but this can crash when you rotate...
1059             Selection.moveToRightEdge(searchText.getText(), searchText.getLayout());
1060         } else {
1061             uiHandler.postDelayed(new Runnable() {
1062                 @Override
1063                 public void run() {
1064                     moveCursorToRight();
1065                 }
1066             }, cursorDelayMillis);
1067             cursorDelayMillis = Math.min(10 * 1000, 2 * cursorDelayMillis);
1068         }
1069     }
1070
1071     // --------------------------------------------------------------------------
1072     // SearchOperation
1073     // --------------------------------------------------------------------------
1074
1075     private void searchFinished(final SearchOperation searchOperation) {
1076         if (searchOperation.interrupted.get()) {
1077             Log.d(LOG, "Search operation was interrupted: " + searchOperation);
1078             return;
1079         }
1080         if (searchOperation != this.currentSearchOperation) {
1081             Log.d(LOG, "Stale searchOperation finished: " + searchOperation);
1082             return;
1083         }
1084
1085         final Index.IndexEntry searchResult = searchOperation.searchResult;
1086         Log.d(LOG, "searchFinished: " + searchOperation + ", searchResult=" + searchResult);
1087
1088         currentSearchOperation = null;
1089         uiHandler.postDelayed(new Runnable() {
1090             @Override
1091             public void run() {
1092                 if (currentSearchOperation == null) {
1093                     if (searchResult != null) {
1094                         if (isFiltered()) {
1095                             clearFiltered();
1096                         }
1097                         jumpToRow(searchResult.startRow);
1098                     } else if (searchOperation.multiWordSearchResult != null) {
1099                         // Multi-row search....
1100                         setFiltered(searchOperation);
1101                     } else {
1102                         throw new IllegalStateException("This should never happen.");
1103                     }
1104                 } else {
1105                     Log.d(LOG, "More coming, waiting for currentSearchOperation.");
1106                 }
1107             }
1108         }, 20);
1109     }
1110
1111     private final void jumpToRow(final int row) {
1112         final boolean refocusSearchText = searchText.hasFocus();
1113         Log.d(LOG, "jumpToRow: " + row + ", refocusSearchText=" + refocusSearchText);
1114         getListView().requestFocusFromTouch();
1115         getListView().setSelectionFromTop(row, 0);
1116         getListView().setSelected(true);
1117         if (refocusSearchText) {            
1118             searchText.requestFocus();
1119         }
1120         //Log.d(LOG, "getSelectedItemPosition():" + getSelectedItemPosition());
1121     }
1122
1123     static final Pattern WHITESPACE = Pattern.compile("\\s+");
1124
1125     final class SearchOperation implements Runnable {
1126
1127         final AtomicBoolean interrupted = new AtomicBoolean(false);
1128
1129         final String searchText;
1130
1131         List<String> searchTokens; // filled in for multiWord.
1132
1133         final Index index;
1134
1135         long searchStartMillis;
1136
1137         Index.IndexEntry searchResult;
1138
1139         List<RowBase> multiWordSearchResult;
1140
1141         boolean done = false;
1142
1143         SearchOperation(final String searchText, final Index index) {
1144             this.searchText = StringUtil.normalizeWhitespace(searchText);
1145             this.index = index;
1146         }
1147
1148         public String toString() {
1149             return String.format("SearchOperation(%s,%s)", searchText, interrupted.toString());
1150         }
1151
1152         @Override
1153         public void run() {
1154             try {
1155                 searchStartMillis = System.currentTimeMillis();
1156                 final String[] searchTokenArray = WHITESPACE.split(searchText);
1157                 if (searchTokenArray.length == 1) {
1158                     searchResult = index.findInsertionPoint(searchText, interrupted);
1159                 } else {
1160                     searchTokens = Arrays.asList(searchTokenArray);
1161                     multiWordSearchResult = index.multiWordSearch(searchText, searchTokens, interrupted);
1162                 }
1163                 Log.d(LOG,
1164                         "searchText=" + searchText + ", searchDuration="
1165                                 + (System.currentTimeMillis() - searchStartMillis)
1166                                 + ", interrupted=" + interrupted.get());
1167                 if (!interrupted.get()) {
1168                     uiHandler.post(new Runnable() {
1169                         @Override
1170                         public void run() {
1171                             searchFinished(SearchOperation.this);
1172                         }
1173                     });
1174                 } else {
1175                     Log.d(LOG, "interrupted, skipping searchFinished.");
1176                 }
1177             } catch (Exception e) {
1178                 Log.e(LOG, "Failure during search (can happen during Activity close.");
1179             } finally {
1180                 synchronized (this) {
1181                     done = true;
1182                     this.notifyAll();
1183                 }
1184             }
1185         }
1186     }
1187
1188     // --------------------------------------------------------------------------
1189     // IndexAdapter
1190     // --------------------------------------------------------------------------
1191
1192     static ViewGroup.LayoutParams WEIGHT_1 = new LinearLayout.LayoutParams(
1193             ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
1194
1195     static ViewGroup.LayoutParams WEIGHT_0 = new LinearLayout.LayoutParams(
1196             ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 0.0f);
1197
1198     final class IndexAdapter extends BaseAdapter {
1199
1200         private static final float PADDING_DEFAULT_DP = 8;
1201
1202         private static final float PADDING_LARGE_DP = 16;
1203
1204         final Index index;
1205
1206         final List<RowBase> rows;
1207
1208         final Set<String> toHighlight;
1209
1210         private int mPaddingDefault;
1211
1212         private int mPaddingLarge;
1213
1214         IndexAdapter(final Index index) {
1215             this.index = index;
1216             rows = index.rows;
1217             this.toHighlight = null;
1218             getMetrics();
1219         }
1220
1221         IndexAdapter(final Index index, final List<RowBase> rows, final List<String> toHighlight) {
1222             this.index = index;
1223             this.rows = rows;
1224             this.toHighlight = new LinkedHashSet<String>(toHighlight);
1225             getMetrics();
1226         }
1227
1228         private void getMetrics() {
1229             // Get the screen's density scale
1230             final float scale = getResources().getDisplayMetrics().density;
1231             // Convert the dps to pixels, based on density scale
1232             mPaddingDefault = (int) (PADDING_DEFAULT_DP * scale + 0.5f);
1233             mPaddingLarge = (int) (PADDING_LARGE_DP * scale + 0.5f);
1234         }
1235
1236         @Override
1237         public int getCount() {
1238             return rows.size();
1239         }
1240
1241         @Override
1242         public RowBase getItem(int position) {
1243             return rows.get(position);
1244         }
1245
1246         @Override
1247         public long getItemId(int position) {
1248             return getItem(position).index();
1249         }
1250
1251         @Override
1252         public TableLayout getView(int position, View convertView, ViewGroup parent) {
1253             final TableLayout result;
1254             if (convertView instanceof TableLayout) {
1255                 result = (TableLayout) convertView;
1256                 result.removeAllViews();
1257             } else {
1258                 result = new TableLayout(parent.getContext());
1259             }
1260             final RowBase row = getItem(position);
1261             if (row instanceof PairEntry.Row) {
1262                 return getView(position, (PairEntry.Row) row, parent, result);
1263             } else if (row instanceof TokenRow) {
1264                 return getView((TokenRow) row, parent, result);
1265             } else if (row instanceof HtmlEntry.Row) {
1266                 return getView((HtmlEntry.Row) row, parent, result);
1267             } else {
1268                 throw new IllegalArgumentException("Unsupported Row type: " + row.getClass());
1269             }
1270         }
1271
1272         private TableLayout getView(final int position, PairEntry.Row row, ViewGroup parent,
1273                 final TableLayout result) {
1274             final PairEntry entry = row.getEntry();
1275             final int rowCount = entry.pairs.size();
1276
1277             final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
1278             layoutParams.weight = 0.5f;
1279             layoutParams.leftMargin = mPaddingLarge;
1280
1281             for (int r = 0; r < rowCount; ++r) {
1282                 final TableRow tableRow = new TableRow(result.getContext());
1283
1284                 final TextView col1 = new TextView(tableRow.getContext());
1285                 final TextView col2 = new TextView(tableRow.getContext());
1286
1287                 // Set the columns in the table.
1288                 if (r > 0) {
1289                     final TextView bullet = new TextView(tableRow.getContext());
1290                     bullet.setText(" • ");
1291                     tableRow.addView(bullet);
1292                 }
1293                 tableRow.addView(col1, layoutParams);
1294                 final TextView margin = new TextView(tableRow.getContext());
1295                 margin.setText(" ");
1296                 tableRow.addView(margin);
1297                 if (r > 0) {
1298                     final TextView bullet = new TextView(tableRow.getContext());
1299                     bullet.setText(" • ");
1300                     tableRow.addView(bullet);
1301                 }
1302                 tableRow.addView(col2, layoutParams);
1303                 col1.setWidth(1);
1304                 col2.setWidth(1);
1305
1306                 // Set what's in the columns.
1307
1308                 final Pair pair = entry.pairs.get(r);
1309                 final String col1Text = index.swapPairEntries ? pair.lang2 : pair.lang1;
1310                 final String col2Text = index.swapPairEntries ? pair.lang1 : pair.lang2;
1311
1312                 col1.setText(col1Text, TextView.BufferType.SPANNABLE);
1313                 col2.setText(col2Text, TextView.BufferType.SPANNABLE);
1314
1315                 // Bold the token instances in col1.
1316                 final Set<String> toBold = toHighlight != null ? this.toHighlight : Collections
1317                         .singleton(row.getTokenRow(true).getToken());
1318                 final Spannable col1Spannable = (Spannable) col1.getText();
1319                 for (final String token : toBold) {
1320                     int startPos = 0;
1321                     while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
1322                         col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos, startPos
1323                                 + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1324                         startPos += token.length();
1325                     }
1326                 }
1327
1328                 createTokenLinkSpans(col1, col1Spannable, col1Text);
1329                 createTokenLinkSpans(col2, (Spannable) col2.getText(), col2Text);
1330
1331                 col1.setTypeface(typeface);
1332                 col2.setTypeface(typeface);
1333                 col1.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);
1334                 col2.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);
1335                 // col2.setBackgroundResource(theme.otherLangBg);
1336
1337                 if (index.swapPairEntries) {
1338                     col2.setOnLongClickListener(textViewLongClickListenerIndex0);
1339                     col1.setOnLongClickListener(textViewLongClickListenerIndex1);
1340                 } else {
1341                     col1.setOnLongClickListener(textViewLongClickListenerIndex0);
1342                     col2.setOnLongClickListener(textViewLongClickListenerIndex1);
1343                 }
1344
1345                 result.addView(tableRow);
1346             }
1347
1348             // Because we have a Button inside a ListView row:
1349             // http://groups.google.com/group/android-developers/browse_thread/thread/3d96af1530a7d62a?pli=1
1350             result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
1351             result.setClickable(true);
1352             result.setFocusable(true);
1353             result.setLongClickable(true);
1354             result.setBackgroundResource(android.R.drawable.menuitem_background);
1355             result.setOnClickListener(new TextView.OnClickListener() {
1356                 @Override
1357                 public void onClick(View v) {
1358                     DictionaryActivity.this.onListItemClick(getListView(), v, position, position);
1359                 }
1360             });
1361
1362             return result;
1363         }
1364
1365         private TableLayout getPossibleLinkToHtmlEntryView(final boolean isTokenRow,
1366                 final String text, final boolean hasMainEntry, final List<HtmlEntry> htmlEntries,
1367                 final String htmlTextToHighlight, ViewGroup parent, final TableLayout result) {
1368             final Context context = parent.getContext();
1369
1370             final TableRow tableRow = new TableRow(result.getContext());
1371             tableRow.setBackgroundResource(hasMainEntry ? theme.tokenRowMainBg
1372                     : theme.tokenRowOtherBg);
1373             if (isTokenRow) {
1374                 tableRow.setPadding(mPaddingDefault, mPaddingDefault, mPaddingDefault, 0);
1375             } else {
1376                 tableRow.setPadding(mPaddingLarge, mPaddingDefault, mPaddingDefault, 0);
1377             }
1378             result.addView(tableRow);
1379
1380             // Make it so we can long-click on these token rows, too:
1381             final TextView textView = new TextView(context);
1382             textView.setText(text, BufferType.SPANNABLE);
1383             createTokenLinkSpans(textView, (Spannable) textView.getText(), text);
1384             final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(
1385                     0);
1386             textView.setOnLongClickListener(textViewLongClickListenerIndex0);
1387             result.setLongClickable(true);
1388             
1389             // Doesn't work:
1390             // textView.setTextColor(android.R.color.secondary_text_light);
1391             textView.setTypeface(typeface);
1392             TableRow.LayoutParams lp = new TableRow.LayoutParams(0);
1393             if (isTokenRow) {
1394                 textView.setTextAppearance(context, theme.tokenRowFg);
1395                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 4 * fontSizeSp / 3);
1396             } else {
1397                 textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSizeSp);
1398             }
1399             lp.weight = 1.0f;
1400
1401             textView.setLayoutParams(lp);
1402             tableRow.addView(textView);
1403
1404             if (!htmlEntries.isEmpty()) {
1405                 final ClickableSpan clickableSpan = new ClickableSpan() {
1406                     @Override
1407                     public void onClick(View widget) {
1408                     }
1409                 };
1410                 ((Spannable) textView.getText()).setSpan(clickableSpan, 0, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1411                 result.setClickable(true);
1412                 textView.setClickable(true);
1413                 textView.setMovementMethod(LinkMovementMethod.getInstance());
1414                 textView.setOnClickListener(new OnClickListener() {
1415                     @Override
1416                     public void onClick(View v) {
1417                         String html = HtmlEntry.htmlBody(htmlEntries, index.shortName);
1418                         //Log.d(LOG, "html=" + html);
1419                         startActivityForResult(
1420                                 HtmlDisplayActivity.getHtmlIntent(String.format(
1421                                         "<html><head></head><body>%s</body></html>", html),
1422                                         htmlTextToHighlight, false),
1423                                 0);
1424                     }
1425                 });
1426             }
1427             return result;
1428         }
1429
1430         private TableLayout getView(TokenRow row, ViewGroup parent, final TableLayout result) {
1431             final IndexEntry indexEntry = row.getIndexEntry();
1432             return getPossibleLinkToHtmlEntryView(true, indexEntry.token, row.hasMainEntry,
1433                     indexEntry.htmlEntries, null, parent, result);
1434         }
1435
1436         private TableLayout getView(HtmlEntry.Row row, ViewGroup parent, final TableLayout result) {
1437             final HtmlEntry htmlEntry = row.getEntry();
1438             final TokenRow tokenRow = row.getTokenRow(true);
1439             return getPossibleLinkToHtmlEntryView(false,
1440                     getString(R.string.seeAlso, htmlEntry.title, htmlEntry.entrySource.getName()),
1441                     false, Collections.singletonList(htmlEntry), tokenRow.getToken(), parent,
1442                     result);
1443         }
1444
1445     }
1446
1447     static final Pattern CHAR_DASH = Pattern.compile("['\\p{L}\\p{M}\\p{N}]+");
1448     
1449     private void createTokenLinkSpans(final TextView textView, final Spannable spannable,
1450             final String text) {
1451         // Saw from the source code that LinkMovementMethod sets the selection!
1452         // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.1_r1/android/text/method/LinkMovementMethod.java#LinkMovementMethod
1453         textView.setMovementMethod(LinkMovementMethod.getInstance());
1454         final Matcher matcher = CHAR_DASH.matcher(text);
1455         while (matcher.find()) {
1456             spannable.setSpan(new NonLinkClickableSpan(textColorFg), matcher.start(), matcher.end(),
1457                     Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
1458         }
1459     }
1460
1461     String selectedSpannableText = null;
1462
1463     int selectedSpannableIndex = -1;
1464
1465     @Override
1466     public boolean onTouchEvent(MotionEvent event) {
1467         selectedSpannableText = null;
1468         selectedSpannableIndex = -1;
1469         return super.onTouchEvent(event);
1470     }
1471
1472     private class TextViewLongClickListener implements OnLongClickListener {
1473         final int index;
1474
1475         private TextViewLongClickListener(final int index) {
1476             this.index = index;
1477         }
1478
1479         @Override
1480         public boolean onLongClick(final View v) {
1481             final TextView textView = (TextView) v;
1482             final int start = textView.getSelectionStart();
1483             final int end = textView.getSelectionEnd();
1484             if (start >= 0 && end >= 0) {
1485                 selectedSpannableText = textView.getText().subSequence(start, end).toString();
1486                 selectedSpannableIndex = index;
1487             }
1488             return false;
1489         }
1490     }
1491
1492     final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(
1493             0);
1494
1495     final TextViewLongClickListener textViewLongClickListenerIndex1 = new TextViewLongClickListener(
1496             1);
1497
1498     // --------------------------------------------------------------------------
1499     // SearchText
1500     // --------------------------------------------------------------------------
1501
1502     void onSearchTextChange(final String text) {
1503         if ("thadolina".equals(text)) {
1504             final Dialog dialog = new Dialog(getListView().getContext());
1505             dialog.setContentView(R.layout.thadolina_dialog);
1506             dialog.setTitle("Ti amo, amore mio!");
1507             final ImageView imageView = (ImageView) dialog.findViewById(R.id.thadolina_image);
1508             imageView.setOnClickListener(new OnClickListener() {
1509                 @Override
1510                 public void onClick(View v) {
1511                     final Intent intent = new Intent(Intent.ACTION_VIEW);
1512                     intent.setData(Uri.parse("https://sites.google.com/site/cfoxroxvday/vday2012"));
1513                     startActivity(intent);
1514                 }
1515             });
1516             dialog.show();
1517         }
1518         if (dictRaf == null) {
1519             Log.d(LOG, "searchText changed during shutdown, doing nothing.");
1520             return;
1521         }
1522         if (!searchText.isFocused()) {
1523             Log.d(LOG, "searchText changed without focus, doing nothing.");
1524             return;
1525         }
1526         Log.d(LOG, "onSearchTextChange: " + text);
1527         if (currentSearchOperation != null) {
1528             Log.d(LOG, "Interrupting currentSearchOperation.");
1529             currentSearchOperation.interrupted.set(true);
1530         }
1531         currentSearchOperation = new SearchOperation(text, index);
1532         searchExecutor.execute(currentSearchOperation);
1533     }
1534
1535     private class SearchTextWatcher implements TextWatcher {
1536         public void afterTextChanged(final Editable searchTextEditable) {
1537             if (searchText.hasFocus()) {
1538                 Log.d(LOG, "SearchTextWatcher: Search text changed with focus: " + searchText.getText());
1539                 // If they were typing to cause the change, update the UI.
1540                 onSearchTextChange(searchText.getText().toString());
1541             }
1542         }
1543
1544         public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
1545         }
1546
1547         public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
1548         }
1549     }
1550
1551     // --------------------------------------------------------------------------
1552     // Filtered results.
1553     // --------------------------------------------------------------------------
1554
1555     boolean isFiltered() {
1556         return rowsToShow != null;
1557     }
1558
1559     void setFiltered(final SearchOperation searchOperation) {
1560         ((ImageButton) findViewById(R.id.UpButton)).setEnabled(false);
1561         ((ImageButton) findViewById(R.id.DownButton)).setEnabled(false);
1562         rowsToShow = searchOperation.multiWordSearchResult;
1563         setListAdapter(new IndexAdapter(index, rowsToShow, searchOperation.searchTokens));
1564     }
1565
1566     void clearFiltered() {
1567         ((ImageButton) findViewById(R.id.UpButton)).setEnabled(true);
1568         ((ImageButton) findViewById(R.id.DownButton)).setEnabled(true);
1569         setListAdapter(new IndexAdapter(index));
1570         rowsToShow = null;
1571     }
1572
1573 }