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