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