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