]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
go
[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     searchOperation.stopAndWait();
208     languageList = null;
209     setListAdapter(null);
210     dictionary = null;
211     try {
212       if (dictRaf != null) {
213         dictRaf.close();
214       }
215     } catch (IOException e) {
216       throw new RuntimeException(e);
217     }
218     dictRaf = null;
219   }
220
221   public String getSelectedRowRawText() {
222     final int row = getSelectedItemPosition();
223     return row < 0 ? "" : languageList.languageData
224         .rowToString(languageList.languageData.rows.get(row));
225   }
226
227   // ----------------------------------------------------------------
228   // OptionsMenu
229   // ----------------------------------------------------------------
230
231   private MenuItem switchLanguageMenuItem = null;
232
233   @Override
234   public boolean onCreateOptionsMenu(final Menu menu) {
235     switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
236     switchLanguageMenuItem
237         .setOnMenuItemClickListener(new OnMenuItemClickListener() {
238           public boolean onMenuItemClick(final MenuItem menuItem) {
239             onLanguageButton();
240             return false;
241           }
242         });
243
244     final MenuItem preferences = menu.add(getString(R.string.preferences));
245     preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
246       public boolean onMenuItemClick(final MenuItem menuItem) {
247         prefsMightHaveChanged = true;
248         startActivity(new Intent(DictionaryActivity.this,
249             PreferenceActivity.class));
250         return false;
251       }
252     });
253
254     final MenuItem about = menu.add(getString(R.string.about));
255     about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
256       public boolean onMenuItemClick(final MenuItem menuItem) {
257         final Intent intent = new Intent().setClassName(AboutActivity.class
258             .getPackage().getName(), AboutActivity.class.getCanonicalName());
259         final StringBuilder currentDictInfo = new StringBuilder();
260         if (dictionary == null) {
261           currentDictInfo.append(getString(R.string.noDictLoaded));
262         } else {
263           currentDictInfo.append(dictionary.dictionaryInfo).append("\n\n");
264           currentDictInfo.append("Entry count: " + dictionary.entries.size())
265               .append("\n");
266           for (int i = 0; i < 2; ++i) {
267             final LanguageData languageData = dictionary.languageDatas[i];
268             currentDictInfo.append(languageData.language.symbol).append(":\n");
269             currentDictInfo.append(
270                 "  Unique token count: " + languageData.sortedIndex.size())
271                 .append("\n");
272             currentDictInfo.append("  Row count: " + languageData.rows.size())
273                 .append("\n");
274           }
275         }
276         intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
277             .toString());
278         startActivity(intent);
279         return false;
280       }
281     });
282
283     final MenuItem download = menu.add(getString(R.string.downloadDictionary));
284     download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
285       public boolean onMenuItemClick(final MenuItem menuItem) {
286         startDownloadDictActivity(DictionaryActivity.this);
287         return false;
288       }
289     });
290
291     return true;
292   }
293
294   @Override
295   public boolean onPrepareOptionsMenu(final Menu menu) {
296     switchLanguageMenuItem.setTitle(String.format(
297         getString(R.string.switchToLanguage), dictionary.languageDatas[Entry
298             .otherLang(languageList.languageData.lang)].language.symbol));
299     return super.onPrepareOptionsMenu(menu);
300   }
301   
302   void updateLangButton() {
303     final Button langButton = (Button) findViewById(R.id.LangButton);
304     langButton.setText(languageList.languageData.language.symbol);
305   }
306
307   // ----------------------------------------------------------------
308   // Event handlers.
309   // ----------------------------------------------------------------
310   
311   void onLanguageButton() {
312     languageList = new LanguageListAdapter(
313         dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
314             : 0]);
315     setListAdapter(languageList);
316     updateLangButton();
317     onSearchTextChange(searchText.getText().toString());
318   }
319
320   void onUpButton() {
321     final int destRowIndex;
322     final Row tokenRow = languageList.languageData.rows
323         .get(selectedTokenRowIndex);
324     assert tokenRow.isToken();
325     final int prevTokenIndex = tokenRow.getIndex() - 1;
326     if (selectedRowIndex == selectedTokenRowIndex && selectedRowIndex > 0) {
327       destRowIndex = languageList.languageData.sortedIndex
328           .get(prevTokenIndex).startRow;
329     } else {
330       destRowIndex = selectedTokenRowIndex;
331     }
332     jumpToRow(languageList, destRowIndex);
333   }
334
335   void onDownButton() {
336     final Row tokenRow = languageList.languageData.rows
337         .get(selectedTokenRowIndex);
338     assert tokenRow.isToken();
339     final int nextTokenIndex = tokenRow.getIndex() + 1;
340     final int destRowIndex;
341     if (nextTokenIndex < languageList.languageData.sortedIndex.size()) {
342       destRowIndex = languageList.languageData.sortedIndex
343           .get(nextTokenIndex).startRow;
344     } else {
345       destRowIndex = languageList.languageData.rows.size() - 1;
346     }
347     jumpToRow(languageList, destRowIndex);
348   }
349
350   void onAppendToWordList() {
351     final int row = getSelectedItemPosition();
352     if (row < 0) {
353       return;
354     }
355     final StringBuilder rawText = new StringBuilder();
356     final String word = languageList.languageData.getIndexEntryForRow(row).word;
357     rawText.append(
358         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
359         .append("\t");
360     rawText.append(word).append("\t");
361     rawText.append(getSelectedRowRawText());
362     Log.d(LOG, "Writing : " + rawText);
363     try {
364       wordList.getParentFile().mkdirs();
365       final PrintWriter out = new PrintWriter(
366           new FileWriter(wordList, true));
367       out.println(rawText.toString());
368       out.close();
369     } catch (IOException e) {
370       Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
371       final AlertDialog alert = new AlertDialog.Builder(
372           DictionaryActivity.this).create();
373       alert.setMessage("Failed to append to file: "
374           + wordList.getAbsolutePath());
375       alert.show();
376     }
377     return;
378   }
379
380   void onCopy() {
381     final int row = getSelectedItemPosition();
382     if (row < 0) {
383       return;
384     }
385     Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedItemPosition());
386     final StringBuilder result = new StringBuilder();
387     result.append(getSelectedRowRawText());
388     final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
389     clipboardManager.setText(result.toString());
390     Log.d(LOG, "Copied: " + result);
391   }
392
393   @Override
394   public boolean onKeyDown(int keyCode, KeyEvent event) {
395     if (event.getUnicodeChar() != 0) {
396       if (!searchText.hasFocus()) {
397         searchText.setText("" + (char) event.getUnicodeChar());
398         onSearchTextChange(searchText.getText().toString());
399         searchText.requestFocus();
400       }
401       return true;
402     }
403     return super.onKeyDown(keyCode, event);
404   }
405
406   @Override
407   protected void onListItemClick(ListView l, View v, int row, long id) {
408     Log.d(LOG, "Clicked: " + getSelectedRowRawText());
409     openContextMenu(getListView());
410   }
411
412   void onSearchTextChange(final String searchText) {
413     Log.d(LOG, "onSearchTextChange: " + searchText);
414     searchOperation = new SearchOperation(languageList, searchText, searchOperation);
415     searchExecutor.execute(searchOperation);
416   }
417
418   // ----------------------------------------------------------------
419   // ContextMenu
420   // ----------------------------------------------------------------
421
422   @Override
423   public void onCreateContextMenu(ContextMenu menu, View v,
424       ContextMenuInfo menuInfo) {
425     final int row = getSelectedItemPosition();
426     if (row < 0) {
427       return;
428     }
429
430     final MenuItem addToWordlist = menu.add(String.format(
431         getString(R.string.addToWordList), wordList.getName()));
432     addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
433       public boolean onMenuItemClick(MenuItem item) {
434         onAppendToWordList();
435         return false;
436       }
437     });
438
439     final MenuItem copy = menu.add(android.R.string.copy);
440     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
441       public boolean onMenuItemClick(MenuItem item) {
442         onCopy();
443         return false;
444       }
445     });
446
447   }
448
449   private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
450       final int rowIndex) {
451     Log.d(LOG, "jumpToRow: " + rowIndex);
452     if (dictionaryListAdapter != this.languageList) {
453       Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
454       return;
455     }
456     // selectedTokenRowIndex =
457     // languageList.languageData.getIndexEntryForRow(rowIndex).startRow;
458     setSelection(rowIndex);
459     getListView().setSelected(true); // TODO: is this doing anything?
460     updateSearchText();
461   }
462
463   private void updateSearchText() {
464     Log.d(LOG, "updateSearchText");
465     if (!searchText.hasFocus()) {
466       final String word = languageList.languageData
467           .getIndexEntryForRow(selectedRowIndex).word;
468       if (!word.equals(searchText.getText().toString())) {
469         Log.d(LOG, "updateSearchText: setText: " + word);
470         searchText.setText(word);
471       }
472     }
473   }
474
475   static void startDownloadDictActivity(final Context context) {
476     final Intent intent = new Intent(context, DownloadActivity.class);
477     final SharedPreferences prefs = PreferenceManager
478         .getDefaultSharedPreferences(context);
479     final String dictFetchUrl = prefs.getString(context
480         .getString(R.string.dictFetchUrlKey), context
481         .getString(R.string.dictFetchUrlDefault));
482     final String dictFileName = prefs.getString(context
483         .getString(R.string.dictFileKey), context
484         .getString(R.string.dictFileDefault));
485     intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
486     intent.putExtra(DownloadActivity.DEST, dictFileName);
487     context.startActivity(intent);
488   }
489
490   class LanguageListAdapter extends BaseAdapter {
491
492     // Visible for testing.
493     final LanguageData languageData;
494
495     LanguageListAdapter(final LanguageData languageData) {
496       this.languageData = languageData;
497     }
498
499     public int getCount() {
500       return languageData.rows.size();
501     }
502
503     public Dictionary.Row getItem(int rowIndex) {
504       assert rowIndex < languageData.rows.size();
505       return languageData.rows.get(rowIndex);
506     }
507
508     public long getItemId(int rowIndex) {
509       return rowIndex;
510     }
511
512     public View getView(final int rowIndex, final View convertView,
513         final ViewGroup parent) {
514       final Row row = getItem(rowIndex);
515
516       // Token row.
517       if (row.isToken()) {
518         TextView result = null;
519         if (convertView instanceof TextView) {
520           result = (TextView) convertView;
521         } else {
522           result = new TextView(parent.getContext());
523         }
524         if (row == null) {
525           return result;
526         }
527         result.setText(languageData.rowToString(row));
528         result.setTextAppearance(parent.getContext(),
529             android.R.style.TextAppearance_Large);
530         result.setClickable(false);
531         return result;
532       }
533
534       // Entry row(s).
535       final TableLayout result = new TableLayout(parent.getContext());
536
537       final Entry entry = dictionary.entries.get(row.getIndex());
538       final int rowCount = entry.getRowCount();
539       for (int r = 0; r < rowCount; ++r) {
540         final TableRow tableRow = new TableRow(result.getContext());
541
542         TextView column1 = new TextView(tableRow.getContext());
543         TextView column2 = new TextView(tableRow.getContext());
544         final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
545         layoutParams.weight = 0.5f;
546
547         if (r > 0) {
548           final TextView spacer = new TextView(tableRow.getContext());
549           spacer.setText(r == 0 ? "\95 " : " \95 ");
550           tableRow.addView(spacer);
551         }
552         tableRow.addView(column1, layoutParams);
553         if (r > 0) {
554           final TextView spacer = new TextView(tableRow.getContext());
555           spacer.setText(r == 0 ? "\95 " : " \95 ");
556           tableRow.addView(spacer);
557         }
558         tableRow.addView(column2, layoutParams);
559
560         column1.setWidth(1);
561         column2.setWidth(1);
562         // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
563
564         // TODO: color words by gender
565         final String col1Text = entry.getAllText(languageData.lang)[r];
566         column1.setText(col1Text, TextView.BufferType.SPANNABLE);
567         final Spannable col1Spannable = (Spannable) column1.getText();
568         int startPos = 0;
569         final String token = languageData.getIndexEntryForRow(rowIndex).word;
570         while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
571           col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
572               startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
573           startPos += token.length();
574         }
575
576         column2.setText(
577             entry.getAllText(Entry.otherLang(languageData.lang))[r],
578             TextView.BufferType.NORMAL);
579
580         result.addView(tableRow);
581       }
582
583       return result;
584     }
585
586   } // DictionaryListAdapter
587
588   private final class SearchOperation implements Runnable {
589     SearchOperation previousSearchOperation;
590     
591     final LanguageListAdapter listAdapter;
592     final LanguageData languageData;
593     final String searchText;
594     final AtomicBoolean interrupted = new AtomicBoolean(false);
595     boolean finished = false;
596
597     SearchOperation(final LanguageListAdapter listAdapter,
598         final String searchText, final SearchOperation previousSearchOperation) {
599       this.listAdapter = listAdapter;
600       this.languageData = listAdapter.languageData;
601       this.searchText = searchText;
602       this.previousSearchOperation = previousSearchOperation;
603     }
604
605     public void run() {
606       if (previousSearchOperation != null) {
607         previousSearchOperation.stopAndWait();
608       }
609       previousSearchOperation = null;
610       
611       Log.d(LOG, "SearchOperation: " + searchText);
612       final int indexLocation = languageData.lookup(searchText, interrupted);
613       if (interrupted.get()) {
614         return;
615       }
616       final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
617       
618       Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
619       uiHandler.post(new Runnable() {
620         public void run() {
621           // Check is just a performance operation.
622           if (!interrupted.get()) {
623             // This is safe, because it checks that the listAdapter hasn't changed.
624             jumpToRow(listAdapter, indexEntry.startRow);
625           }
626         }
627       });
628       
629       synchronized (this) {
630         finished = true;
631         this.notifyAll();
632       }
633     }
634     
635     public void stopAndWait() {
636       interrupted.set(true);
637       synchronized (this) {
638         while (!finished) {
639           Log.d(LOG, "stopAndWait: " + searchText);
640           try {
641             this.wait();
642           } catch (InterruptedException e) {
643             Log.e(LOG, "Interrupted", e);
644           }
645         }
646       }
647     }
648
649
650   }
651
652   private class SearchTextWatcher implements TextWatcher {
653     public void afterTextChanged(final Editable searchTextEditable) {
654       Log.d(LOG, "Search text changed: " + searchText.getText().toString());
655       if (searchText.hasFocus()) {
656         // If they were typing to cause the change, update the UI.
657         onSearchTextChange(searchText.getText().toString());
658       }
659     }
660
661     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
662         int arg3) {
663     }
664
665     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
666     }
667   }
668
669 }