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