]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
155a15cad207653f24cdbfa5c8b0d873fb6fd817
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 package com.hughes.android.dictionary;
2
3 import java.io.File;
4 import java.io.FileWriter;
5 import java.io.IOException;
6 import java.io.PrintWriter;
7 import java.io.RandomAccessFile;
8 import java.text.SimpleDateFormat;
9 import java.util.Date;
10 import java.util.concurrent.Executor;
11 import java.util.concurrent.Executors;
12 import java.util.concurrent.atomic.AtomicBoolean;
13
14 import android.app.AlertDialog;
15 import android.app.ListActivity;
16 import android.content.Context;
17 import android.content.Intent;
18 import android.content.SharedPreferences;
19 import android.content.SharedPreferences.Editor;
20 import android.graphics.Typeface;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.preference.PreferenceManager;
24 import android.text.ClipboardManager;
25 import android.text.Editable;
26 import android.text.Spannable;
27 import android.text.TextWatcher;
28 import android.text.style.StyleSpan;
29 import android.util.Log;
30 import android.view.ContextMenu;
31 import android.view.KeyEvent;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ContextMenu.ContextMenuInfo;
37 import android.view.MenuItem.OnMenuItemClickListener;
38 import android.view.View.OnClickListener;
39 import android.widget.BaseAdapter;
40 import android.widget.Button;
41 import android.widget.EditText;
42 import android.widget.ListView;
43 import android.widget.TableLayout;
44 import android.widget.TableRow;
45 import android.widget.TextView;
46
47 import com.hughes.android.dictionary.Dictionary.IndexEntry;
48 import com.hughes.android.dictionary.Dictionary.LanguageData;
49 import com.hughes.android.dictionary.Dictionary.Row;
50
51 public class DictionaryActivity extends ListActivity {
52   
53   // TODO:
54   // * Only have one live SearchActivity, and a way to wait for it to die.
55   // * Don't destroy dict unless we're really shutting down (not on screen rotate).
56   // * Move (re-)init code to a method, set a flag if prefs might have changed, invoke re-init in onResume, which clears flag and reloads prefs.
57   // * Compress all the strings everywhere, put compression table in file.
58
59   static final String LOG = "QuickDic";
60   static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
61   static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
62
63   // package for test.
64   final Handler uiHandler = new Handler();
65
66   EditText searchText;
67
68   private final Executor searchExecutor = Executors.newSingleThreadExecutor();
69
70   // Never null.
71   private boolean prefsMightHaveChanged = true;
72   private File wordList;
73
74   private RandomAccessFile dictRaf = null;
75   private Dictionary dictionary = null;
76
77   // Visible for testing.
78   LanguageListAdapter languageList = null;
79
80   private SearchOperation searchOperation = null;
81
82   private int selectedRowIndex;
83   private int selectedTokenRowIndex;
84
85   /** Called when the activity is first created. */
86   @Override
87   public void onCreate(Bundle savedInstanceState) {
88     super.onCreate(savedInstanceState);
89     Log.d(LOG, "onCreate");
90
91     if (Language.EN.sortCollator.compare("pre-print", "preppy") >= 0) {
92       Log
93           .e(LOG,
94               "Android java.text.Collator is buggy, lookups may not work properly.");
95     }
96
97     initDictionaryAndPrefs();
98     if (dictRaf == null) {
99       return;
100     }
101
102     // UI init.
103
104     setContentView(R.layout.main);
105     searchText = (EditText) findViewById(R.id.SearchText);
106
107     Log.d(LOG, "adding text changed listener");
108     searchText.addTextChangedListener(new SearchTextWatcher());
109
110     // Language button.
111     final Button langButton = (Button) findViewById(R.id.LangButton);
112     langButton.setOnClickListener(new OnClickListener() {
113       public void onClick(View v) {
114         onLanguageButton();
115       }
116     });
117     final Button upButton = (Button) findViewById(R.id.UpButton);
118     upButton.setOnClickListener(new OnClickListener() {
119       public void onClick(View v) {
120         onUpButton();
121       }
122     });
123     final Button downButton = (Button) findViewById(R.id.DownButton);
124     downButton.setOnClickListener(new OnClickListener() {
125       public void onClick(View v) {
126         onDownButton();
127       }
128     });
129
130     // ContextMenu.
131     registerForContextMenu(getListView());
132
133     updateLangButton();
134   }
135
136   private void initDictionaryAndPrefs() {
137     if (!prefsMightHaveChanged) {
138       return;
139     }
140     closeCurrentDictionary();
141     
142     final SharedPreferences prefs = PreferenceManager
143         .getDefaultSharedPreferences(this);
144     wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
145         getString(R.string.wordListFileDefault)));
146     Log.d(LOG, "wordList=" + wordList);
147
148     final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
149         getString(R.string.dictFileDefault)));
150     Log.d(LOG, "dictFile=" + dictFile);
151     if (!dictFile.canRead()) {
152       Log.w(LOG, "Unable to read dictionary file.");
153       this.startActivity(new Intent(this, NoDictionaryActivity.class));
154       finish();
155     }
156
157     try {
158       dictRaf = new RandomAccessFile(dictFile, "r");
159       dictionary = new Dictionary(dictRaf);
160     } catch (Exception e) {
161       throw new RuntimeException(e);
162     }
163
164     final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
165         : Entry.LANG2;
166
167     languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
168     setListAdapter(languageList);
169     prefsMightHaveChanged = false;
170   }
171
172   @Override
173   public void onResume() {
174     super.onResume();
175     
176     if (prefsMightHaveChanged) {
177       
178     }
179     
180     final SharedPreferences prefs = PreferenceManager
181         .getDefaultSharedPreferences(this);
182     final String searchTextString = prefs
183         .getString(PREF_ACTIVE_SEARCH_TEXT, "");
184     searchText.setText(searchTextString);
185     onSearchTextChange(searchTextString);
186   }
187
188   @Override
189   public void onPause() {
190     super.onPause();
191     final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
192         .edit();
193     prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
194     prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
195     prefs.commit();
196   }
197
198   @Override
199   public void onStop() {
200     super.onStop();
201     if (isFinishing()) {
202       closeCurrentDictionary();
203     }
204   }
205
206   private void closeCurrentDictionary() {
207     Log.i(LOG, "closeCurrentDictionary");
208     searchOperation.stopAndWait();
209     languageList = null;
210     setListAdapter(null);
211     dictionary = null;
212     try {
213       if (dictRaf != null) {
214         dictRaf.close();
215       }
216     } catch (IOException e) {
217       throw new RuntimeException(e);
218     }
219     dictRaf = null;
220   }
221
222   public String getSelectedRowRawText() {
223     final int row = getSelectedItemPosition();
224     return row < 0 ? "" : languageList.languageData
225         .rowToString(languageList.languageData.rows.get(row));
226   }
227
228   // ----------------------------------------------------------------
229   // OptionsMenu
230   // ----------------------------------------------------------------
231
232   private MenuItem switchLanguageMenuItem = null;
233
234   @Override
235   public boolean onCreateOptionsMenu(final Menu menu) {
236     switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
237     switchLanguageMenuItem
238         .setOnMenuItemClickListener(new OnMenuItemClickListener() {
239           public boolean onMenuItemClick(final MenuItem menuItem) {
240             onLanguageButton();
241             return false;
242           }
243         });
244
245     final MenuItem preferences = menu.add(getString(R.string.preferences));
246     preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
247       public boolean onMenuItemClick(final MenuItem menuItem) {
248         prefsMightHaveChanged = true;
249         startActivity(new Intent(DictionaryActivity.this,
250             PreferenceActivity.class));
251         return false;
252       }
253     });
254
255     final MenuItem about = menu.add(getString(R.string.about));
256     about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
257       public boolean onMenuItemClick(final MenuItem menuItem) {
258         final Intent intent = new Intent().setClassName(AboutActivity.class
259             .getPackage().getName(), AboutActivity.class.getCanonicalName());
260         final StringBuilder currentDictInfo = new StringBuilder();
261         if (dictionary == null) {
262           currentDictInfo.append(getString(R.string.noDictLoaded));
263         } else {
264           currentDictInfo.append(dictionary.dictionaryInfo).append("\n\n");
265           currentDictInfo.append("Entry count: " + dictionary.entries.size())
266               .append("\n");
267           for (int i = 0; i < 2; ++i) {
268             final LanguageData languageData = dictionary.languageDatas[i];
269             currentDictInfo.append(languageData.language.symbol).append(":\n");
270             currentDictInfo.append(
271                 "  Unique token count: " + languageData.sortedIndex.size())
272                 .append("\n");
273             currentDictInfo.append("  Row count: " + languageData.rows.size())
274                 .append("\n");
275           }
276         }
277         intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
278             .toString());
279         startActivity(intent);
280         return false;
281       }
282     });
283
284     final MenuItem download = menu.add(getString(R.string.downloadDictionary));
285     download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
286       public boolean onMenuItemClick(final MenuItem menuItem) {
287         startDownloadDictActivity(DictionaryActivity.this);
288         return false;
289       }
290     });
291
292     return true;
293   }
294
295   @Override
296   public boolean onPrepareOptionsMenu(final Menu menu) {
297     switchLanguageMenuItem.setTitle(String.format(
298         getString(R.string.switchToLanguage), dictionary.languageDatas[Entry
299             .otherLang(languageList.languageData.lang)].language.symbol));
300     return super.onPrepareOptionsMenu(menu);
301   }
302   
303   void updateLangButton() {
304     final Button langButton = (Button) findViewById(R.id.LangButton);
305     langButton.setText(languageList.languageData.language.symbol);
306   }
307
308   // ----------------------------------------------------------------
309   // Event handlers.
310   // ----------------------------------------------------------------
311   
312   void onLanguageButton() {
313     languageList = new LanguageListAdapter(
314         dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
315             : 0]);
316     setListAdapter(languageList);
317     updateLangButton();
318     onSearchTextChange(searchText.getText().toString());
319   }
320
321   void onUpButton() {
322     final int destRowIndex;
323     final Row tokenRow = languageList.languageData.rows
324         .get(selectedTokenRowIndex);
325     assert tokenRow.isToken();
326     final int prevTokenIndex = tokenRow.getIndex() - 1;
327     if (selectedRowIndex == selectedTokenRowIndex && selectedRowIndex > 0) {
328       destRowIndex = languageList.languageData.sortedIndex
329           .get(prevTokenIndex).startRow;
330     } else {
331       destRowIndex = selectedTokenRowIndex;
332     }
333     jumpToRow(languageList, destRowIndex);
334   }
335
336   void onDownButton() {
337     final Row tokenRow = languageList.languageData.rows
338         .get(selectedTokenRowIndex);
339     assert tokenRow.isToken();
340     final int nextTokenIndex = tokenRow.getIndex() + 1;
341     final int destRowIndex;
342     if (nextTokenIndex < languageList.languageData.sortedIndex.size()) {
343       destRowIndex = languageList.languageData.sortedIndex
344           .get(nextTokenIndex).startRow;
345     } else {
346       destRowIndex = languageList.languageData.rows.size() - 1;
347     }
348     jumpToRow(languageList, destRowIndex);
349   }
350
351   void onAppendToWordList() {
352     final int row = getSelectedItemPosition();
353     if (row < 0) {
354       return;
355     }
356     final StringBuilder rawText = new StringBuilder();
357     final String word = languageList.languageData.getIndexEntryForRow(row).word;
358     rawText.append(
359         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
360         .append("\t");
361     rawText.append(word).append("\t");
362     rawText.append(getSelectedRowRawText());
363     Log.d(LOG, "Writing : " + rawText);
364     try {
365       wordList.getParentFile().mkdirs();
366       final PrintWriter out = new PrintWriter(
367           new FileWriter(wordList, true));
368       out.println(rawText.toString());
369       out.close();
370     } catch (IOException e) {
371       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
372       final AlertDialog alert = new AlertDialog.Builder(
373           DictionaryActivity.this).create();
374       alert.setMessage("Failed to append to file: "
375           + wordList.getAbsolutePath());
376       alert.show();
377     }
378     return;
379   }
380
381   void onCopy() {
382     final int row = getSelectedItemPosition();
383     if (row < 0) {
384       return;
385     }
386     Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedItemPosition());
387     final StringBuilder result = new StringBuilder();
388     result.append(getSelectedRowRawText());
389     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
390     clipboardManager.setText(result.toString());
391     Log.d(LOG, "Copied: " + result);
392   }
393
394   @Override
395   public boolean onKeyDown(int keyCode, KeyEvent event) {
396     if (event.getUnicodeChar() != 0) {
397       if (!searchText.hasFocus()) {
398         searchText.setText("" + (char) event.getUnicodeChar());
399         onSearchTextChange(searchText.getText().toString());
400         searchText.requestFocus();
401       }
402       return true;
403     }
404     return super.onKeyDown(keyCode, event);
405   }
406
407   @Override
408   protected void onListItemClick(ListView l, View v, int row, long id) {
409     Log.d(LOG, "Clicked: " + getSelectedRowRawText());
410     openContextMenu(getListView());
411   }
412
413   void onSearchTextChange(final String searchText) {
414     Log.d(LOG, "onSearchTextChange: " + searchText);
415     searchOperation = new SearchOperation(languageList, searchText, searchOperation);
416     searchExecutor.execute(searchOperation);
417   }
418
419   // ----------------------------------------------------------------
420   // ContextMenu
421   // ----------------------------------------------------------------
422
423   @Override
424   public void onCreateContextMenu(ContextMenu menu, View v,
425       ContextMenuInfo menuInfo) {
426     final int row = getSelectedItemPosition();
427     if (row < 0) {
428       return;
429     }
430
431     final MenuItem addToWordlist = menu.add(String.format(
432         getString(R.string.addToWordList), wordList.getName()));
433     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
434       public boolean onMenuItemClick(MenuItem item) {
435         onAppendToWordList();
436         return false;
437       }
438     });
439
440     final MenuItem copy = menu.add(android.R.string.copy);
441     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
442       public boolean onMenuItemClick(MenuItem item) {
443         onCopy();
444         return false;
445       }
446     });
447
448   }
449
450   private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
451       final int rowIndex) {
452     Log.d(LOG, "jumpToRow: " + rowIndex);
453     if (dictionaryListAdapter != this.languageList) {
454       Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
455       return;
456     }
457     // selectedTokenRowIndex =
458     // languageList.languageData.getIndexEntryForRow(rowIndex).startRow;
459     setSelection(rowIndex);
460     getListView().setSelected(true); // TODO: is this doing anything?
461     updateSearchText();
462   }
463
464   private void updateSearchText() {
465     Log.d(LOG, "updateSearchText");
466     if (!searchText.hasFocus()) {
467       final String word = languageList.languageData
468           .getIndexEntryForRow(selectedRowIndex).word;
469       if (!word.equals(searchText.getText().toString())) {
470         Log.d(LOG, "updateSearchText: setText: " + word);
471         searchText.setText(word);
472       }
473     }
474   }
475
476   static void startDownloadDictActivity(final Context context) {
477     final Intent intent = new Intent(context, DownloadActivity.class);
478     final SharedPreferences prefs = PreferenceManager
479         .getDefaultSharedPreferences(context);
480     final String dictFetchUrl = prefs.getString(context
481         .getString(R.string.dictFetchUrlKey), context
482         .getString(R.string.dictFetchUrlDefault));
483     final String dictFileName = prefs.getString(context
484         .getString(R.string.dictFileKey), context
485         .getString(R.string.dictFileDefault));
486     intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
487     intent.putExtra(DownloadActivity.DEST, dictFileName);
488     context.startActivity(intent);
489   }
490
491   class LanguageListAdapter extends BaseAdapter {
492
493     // Visible for testing.
494     final LanguageData languageData;
495
496     LanguageListAdapter(final LanguageData languageData) {
497       this.languageData = languageData;
498     }
499
500     public int getCount() {
501       return languageData.rows.size();
502     }
503
504     public Dictionary.Row getItem(int rowIndex) {
505       assert rowIndex < languageData.rows.size();
506       return languageData.rows.get(rowIndex);
507     }
508
509     public long getItemId(int rowIndex) {
510       return rowIndex;
511     }
512
513     public View getView(final int rowIndex, final View convertView,
514         final ViewGroup parent) {
515       final Row row = getItem(rowIndex);
516
517       // Token row.
518       if (row.isToken()) {
519         TextView result = null;
520         if (convertView instanceof TextView) {
521           result = (TextView) convertView;
522         } else {
523           result = new TextView(parent.getContext());
524         }
525         if (row == null) {
526           return result;
527         }
528         result.setText(languageData.rowToString(row));
529         result.setTextAppearance(parent.getContext(),
530             android.R.style.TextAppearance_Large);
531         result.setClickable(false);
532         return result;
533       }
534
535       // Entry row(s).
536       final TableLayout result = new TableLayout(parent.getContext());
537
538       final Entry entry = dictionary.entries.get(row.getIndex());
539       final int rowCount = entry.getRowCount();
540       for (int r = 0; r < rowCount; ++r) {
541         final TableRow tableRow = new TableRow(result.getContext());
542
543         TextView column1 = new TextView(tableRow.getContext());
544         TextView column2 = new TextView(tableRow.getContext());
545         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
546         layoutParams.weight = 0.5f;
547
548         if (r > 0) {
549           final TextView spacer = new TextView(tableRow.getContext());
550           spacer.setText(r == 0 ? "\95 " : " \95 ");
551           tableRow.addView(spacer);
552         }
553         tableRow.addView(column1, layoutParams);
554         if (r > 0) {
555           final TextView spacer = new TextView(tableRow.getContext());
556           spacer.setText(r == 0 ? "\95 " : " \95 ");
557           tableRow.addView(spacer);
558         }
559         tableRow.addView(column2, layoutParams);
560
561         column1.setWidth(1);
562         column2.setWidth(1);
563         // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
564
565         // TODO: color words by gender
566         final String col1Text = entry.getAllText(languageData.lang)[r];
567         column1.setText(col1Text, TextView.BufferType.SPANNABLE);
568         final Spannable col1Spannable = (Spannable) column1.getText();
569         int startPos = 0;
570         final String token = languageData.getIndexEntryForRow(rowIndex).word;
571         while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
572           col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
573               startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
574           startPos += token.length();
575         }
576
577         column2.setText(
578             entry.getAllText(Entry.otherLang(languageData.lang))[r],
579             TextView.BufferType.NORMAL);
580
581         result.addView(tableRow);
582       }
583
584       return result;
585     }
586
587   } // DictionaryListAdapter
588
589   private final class SearchOperation implements Runnable {
590     SearchOperation previousSearchOperation;
591     
592     final LanguageListAdapter listAdapter;
593     final LanguageData languageData;
594     final String searchText;
595     final AtomicBoolean interrupted = new AtomicBoolean(false);
596     boolean finished = false;
597
598     SearchOperation(final LanguageListAdapter listAdapter,
599         final String searchText, final SearchOperation previousSearchOperation) {
600       this.listAdapter = listAdapter;
601       this.languageData = listAdapter.languageData;
602       this.searchText = searchText;
603       this.previousSearchOperation = previousSearchOperation;
604     }
605
606     public void run() {
607       if (previousSearchOperation != null) {
608         previousSearchOperation.stopAndWait();
609       }
610       previousSearchOperation = null;
611       
612       Log.d(LOG, "SearchOperation: " + searchText);
613       final int indexLocation = languageData.lookup(searchText, interrupted);
614       if (interrupted.get()) {
615         return;
616       }
617       final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
618       
619       Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
620       uiHandler.post(new Runnable() {
621         public void run() {
622           // Check is just a performance operation.
623           if (!interrupted.get()) {
624             // This is safe, because it checks that the listAdapter hasn't changed.
625             jumpToRow(listAdapter, indexEntry.startRow);
626           }
627         }
628       });
629       
630       synchronized (this) {
631         finished = true;
632         this.notifyAll();
633       }
634     }
635     
636     public void stopAndWait() {
637       interrupted.set(true);
638       synchronized (this) {
639         while (!finished) {
640           Log.d(LOG, "stopAndWait: " + searchText);
641           try {
642             this.wait();
643           } catch (InterruptedException e) {
644             Log.e(LOG, "Interrupted", e);
645           }
646         }
647       }
648     }
649
650
651   }
652
653   private class SearchTextWatcher implements TextWatcher {
654     public void afterTextChanged(final Editable searchTextEditable) {
655       Log.d(LOG, "Search text changed: " + searchText.getText().toString());
656       if (searchText.hasFocus()) {
657         // If they were typing to cause the change, update the UI.
658         onSearchTextChange(searchText.getText().toString());
659       }
660     }
661
662     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
663         int arg3) {
664     }
665
666     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
667     }
668   }
669
670 }