]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryApplication.java
6cfa207bb9125570f7ac8a2bd85190f75ad5452e
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryApplication.java
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package com.hughes.android.dictionary;
16
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.SharedPreferences;
20 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
21 import android.net.Uri;
22 import android.os.Build;
23 import android.os.Environment;
24 import android.preference.PreferenceManager;
25 import android.support.v4.view.MenuItemCompat;
26 import android.util.Log;
27 import android.util.TypedValue;
28 import android.view.Menu;
29 import android.view.MenuItem;
30 import android.view.MenuItem.OnMenuItemClickListener;
31
32 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;
33 import com.hughes.android.dictionary.engine.Dictionary;
34 import com.hughes.android.dictionary.engine.TransliteratorManager;
35 import com.hughes.android.util.PersistentObjectCache;
36 import com.hughes.util.ListUtil;
37
38 import java.io.BufferedReader;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.io.Serializable;
43 import java.util.ArrayList;
44 import java.util.Comparator;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.Map;
49
50 public enum DictionaryApplication {
51     INSTANCE;
52
53     private Context appContext;
54
55     static final String LOG = "QuickDicApp";
56
57     // If set to false, avoid use of ICU collator
58     // Works well enough for most european languages,
59     // gives faster startup and avoids crashes on some
60     // devices due to Dalvik bugs (e.g. ARMv6, S5570i, CM11)
61     // when using ICU4J.
62     // Leave it enabled by default for correctness except
63     // for my known broken development/performance test device config.
64     //static public final boolean USE_COLLATOR = !android.os.Build.FINGERPRINT.equals("Samsung/cm_tassve/tassve:4.4.4/KTU84Q/20150211:userdebug/release-keys");
65     public static final boolean USE_COLLATOR = true;
66
67     public static final TransliteratorManager.ThreadSetup threadBackground = new TransliteratorManager.ThreadSetup() {
68         @Override
69         public void onThreadStart() {
70             // THREAD_PRIORITY_BACKGROUND seemed like a good idea, but it
71             // can make Transliterator go from 20 seconds to 3 minutes (!)
72             android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
73         }
74     };
75
76     // Static, determined by resources (and locale).
77     // Unordered.
78     static Map<String, DictionaryInfo> DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO = null;
79
80     enum Theme {
81         DEFAULT(R.style.Theme_Default,
82         R.style.Theme_Default_TokenRow_Fg,
83         R.color.theme_default_token_row_fg,
84         R.drawable.theme_default_token_row_main_bg,
85         R.drawable.theme_default_token_row_other_bg,
86         R.drawable.theme_default_normal_row_bg),
87
88         LIGHT(R.style.Theme_Light,
89         R.style.Theme_Light_TokenRow_Fg,
90         R.color.theme_light_token_row_fg,
91         R.drawable.theme_light_token_row_main_bg,
92         R.drawable.theme_light_token_row_other_bg,
93         R.drawable.theme_light_normal_row_bg);
94
95         Theme(final int themeId, final int tokenRowFg,
96         final int tokenRowFgColor,
97         final int tokenRowMainBg, final int tokenRowOtherBg,
98         final int normalRowBg) {
99             this.themeId = themeId;
100             this.tokenRowFg = tokenRowFg;
101             this.tokenRowFgColor = tokenRowFgColor;
102             this.tokenRowMainBg = tokenRowMainBg;
103             this.tokenRowOtherBg = tokenRowOtherBg;
104             this.normalRowBg = normalRowBg;
105         }
106
107         final int themeId;
108         final int tokenRowFg;
109         final int tokenRowFgColor;
110         final int tokenRowMainBg;
111         final int tokenRowOtherBg;
112         final int normalRowBg;
113     }
114
115     public static final class DictionaryConfig implements Serializable {
116         private static final long serialVersionUID = -1444177164708201263L;
117         // User-ordered list, persisted, just the ones that are/have been
118         // present.
119         final List<String> dictionaryFilesOrdered = new ArrayList<>();
120
121         final Map<String, DictionaryInfo> uncompressedFilenameToDictionaryInfo = new HashMap<>();
122
123         /**
124          * Sometimes a deserialized version of this data structure isn't valid.
125          */
126         @SuppressWarnings("ConstantConditions")
127         boolean isValid() {
128             return uncompressedFilenameToDictionaryInfo != null && dictionaryFilesOrdered != null;
129         }
130     }
131
132     DictionaryConfig dictionaryConfig = null;
133
134     public int languageButtonPixels = -1;
135
136     static synchronized void staticInit(final Context context) {
137         if (DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO != null) {
138             return;
139         }
140         DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO = new HashMap<>();
141         final BufferedReader reader = new BufferedReader(
142             new InputStreamReader(context.getResources().openRawResource(R.raw.dictionary_info)));
143         try {
144             String line;
145             while ((line = reader.readLine()) != null) {
146                 if (line.length() == 0 || line.charAt(0) == '#') {
147                     continue;
148                 }
149                 final DictionaryInfo dictionaryInfo = new DictionaryInfo(line);
150                 DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.put(
151                     dictionaryInfo.uncompressedFilename, dictionaryInfo);
152             }
153         } catch (IOException e) {
154             Log.e(LOG, "Failed to load downloadable dictionary lists.", e);
155         }
156         try {
157             reader.close();
158         } catch (IOException ignored) {}
159     }
160
161     public synchronized void init(Context c) {
162         if (appContext != null) {
163             assert c == appContext;
164             return;
165         }
166         appContext = c;
167         Log.d("QuickDic", "Application: onCreate");
168         TransliteratorManager.init(null, threadBackground);
169         staticInit(appContext);
170
171         languageButtonPixels = (int) TypedValue.applyDimension(
172                                    TypedValue.COMPLEX_UNIT_DIP, 60, appContext.getResources().getDisplayMetrics());
173
174         // Load the dictionaries we know about.
175         dictionaryConfig = PersistentObjectCache.init(appContext).read(
176                                C.DICTIONARY_CONFIGS, DictionaryConfig.class);
177         if (dictionaryConfig == null) {
178             dictionaryConfig = new DictionaryConfig();
179         }
180         if (!dictionaryConfig.isValid()) {
181             dictionaryConfig = new DictionaryConfig();
182         }
183
184         // Theme stuff.
185         appContext.setTheme(getSelectedTheme().themeId);
186         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
187         prefs.registerOnSharedPreferenceChangeListener(new OnSharedPreferenceChangeListener() {
188             @Override
189             public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
190                                                   String key) {
191                 Log.d("QuickDic", "prefs changed: " + key);
192                 if (key.equals(appContext.getString(R.string.themeKey))) {
193                     appContext.setTheme(getSelectedTheme().themeId);
194                 }
195             }
196         });
197     }
198
199     public static void onCreateGlobalOptionsMenu(
200         final Context context, final Menu menu) {
201         final Context c = context.getApplicationContext();
202
203         final MenuItem preferences = menu.add(c.getString(R.string.settings));
204         MenuItemCompat.setShowAsAction(preferences, MenuItem.SHOW_AS_ACTION_NEVER);
205         preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
206             public boolean onMenuItemClick(final MenuItem menuItem) {
207                 PreferenceActivity.prefsMightHaveChanged = true;
208                 final Intent intent = new Intent(c, PreferenceActivity.class);
209                 context.startActivity(intent);
210                 return false;
211             }
212         });
213
214         final MenuItem help = menu.add(c.getString(R.string.help));
215         MenuItemCompat.setShowAsAction(help, MenuItem.SHOW_AS_ACTION_NEVER);
216         help.setOnMenuItemClickListener(new OnMenuItemClickListener() {
217             public boolean onMenuItemClick(final MenuItem menuItem) {
218                 context.startActivity(HtmlDisplayActivity.getHelpLaunchIntent(c));
219                 return false;
220             }
221         });
222
223         final MenuItem reportIssue = menu.add(c.getString(R.string.reportIssue));
224         MenuItemCompat.setShowAsAction(reportIssue, MenuItem.SHOW_AS_ACTION_NEVER);
225         reportIssue.setOnMenuItemClickListener(new OnMenuItemClickListener() {
226             public boolean onMenuItemClick(final MenuItem menuItem) {
227                 final Intent intent = new Intent(Intent.ACTION_VIEW);
228                 intent.setData(Uri
229                                .parse("https://github.com/rdoeffinger/Dictionary/issues"));
230                 context.startActivity(intent);
231                 return false;
232             }
233         });
234
235         final MenuItem about = menu.add(c.getString(R.string.about));
236         MenuItemCompat.setShowAsAction(about, MenuItem.SHOW_AS_ACTION_NEVER);
237         about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
238             public boolean onMenuItemClick(final MenuItem menuItem) {
239                 final Intent intent = new Intent(c, AboutActivity.class);
240                 context.startActivity(intent);
241                 return false;
242             }
243         });
244     }
245
246     private String selectDefaultDir() {
247         final File defaultDictDir = new File(Environment.getExternalStorageDirectory(), "quickDic");
248         String dir = defaultDictDir.getAbsolutePath();
249         File dictDir = new File(dir);
250         String[] fileList = dictDir.isDirectory() ? dictDir.list() : null;
251         if (fileList != null && fileList.length > 0) {
252             return dir;
253         }
254         File efd = null;
255         try {
256             efd = appContext.getExternalFilesDir(null);
257         } catch (Exception ignored) {
258         }
259         if (efd != null) {
260             efd.mkdirs();
261             if (!dictDir.isDirectory() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
262                 appContext.getExternalFilesDirs(null);
263             }
264             if (efd.isDirectory() && efd.canWrite() && checkFileCreate(efd)) {
265                 return efd.getAbsolutePath();
266             }
267         }
268         if (!dictDir.isDirectory() && !dictDir.mkdirs()) {
269             return appContext.getFilesDir().getAbsolutePath();
270         }
271         return dir;
272     }
273
274     public synchronized File getDictDir() {
275         // This metaphor doesn't work, because we've already reset
276         // prefsMightHaveChanged.
277         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
278         String dir = prefs.getString(appContext.getString(R.string.quickdicDirectoryKey), "");
279         if (dir.isEmpty()) {
280             dir = selectDefaultDir();
281         }
282         File dictDir = new File(dir);
283         dictDir.mkdirs();
284         if (!dictDir.isDirectory() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
285             appContext.getExternalFilesDirs(null);
286         }
287         return dictDir;
288     }
289
290     public static boolean checkFileCreate(File dir) {
291         boolean res = false;
292         File testfile = new File(dir, "quickdic_writetest");
293         try {
294             testfile.delete();
295             res = testfile.createNewFile() & testfile.delete();
296         } catch (Exception ignored) {
297         }
298         return res;
299     }
300
301     public File getWordListFile() {
302         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
303         String file = prefs.getString(appContext.getString(R.string.wordListFileKey), "");
304         if (file.isEmpty()) {
305             return new File(getDictDir(), "wordList.txt");
306         }
307         return new File(file);
308     }
309
310     public Theme getSelectedTheme() {
311         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext);
312         final String theme = prefs.getString(appContext.getString(R.string.themeKey), "themeLight");
313         if (theme.equals("themeLight")) {
314             return Theme.LIGHT;
315         } else {
316             return Theme.DEFAULT;
317         }
318     }
319
320     public File getPath(String uncompressedFilename) {
321         return new File(getDictDir(), uncompressedFilename);
322     }
323
324     String defaultLangISO2 = Locale.getDefault().getLanguage().toLowerCase();
325     String defaultLangName = null;
326     final Map<String, String> fileToNameCache = new HashMap<>();
327
328     public List<IndexInfo> sortedIndexInfos(List<IndexInfo> indexInfos) {
329         // Hack to put the default locale first in the name.
330         if (indexInfos.size() > 1 &&
331                 indexInfos.get(1).shortName.toLowerCase().equals(defaultLangISO2)) {
332             List<IndexInfo> result = new ArrayList<>(indexInfos);
333             ListUtil.swap(result, 0, 1);
334             return result;
335         }
336         return indexInfos;
337     }
338
339     public synchronized String getDictionaryName(final String uncompressedFilename) {
340         final String currentLocale = Locale.getDefault().getLanguage().toLowerCase();
341         if (!currentLocale.equals(defaultLangISO2)) {
342             defaultLangISO2 = currentLocale;
343             fileToNameCache.clear();
344             defaultLangName = null;
345         }
346         if (defaultLangName == null) {
347             defaultLangName = IsoUtils.INSTANCE.isoCodeToLocalizedLanguageName(appContext, defaultLangISO2);
348         }
349
350         String name = fileToNameCache.get(uncompressedFilename);
351         if (name != null) {
352             return name;
353         }
354
355         final DictionaryInfo dictionaryInfo = DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO
356                                               .get(uncompressedFilename);
357         if (dictionaryInfo != null) {
358             final StringBuilder nameBuilder = new StringBuilder();
359
360             List<IndexInfo> sortedIndexInfos = sortedIndexInfos(dictionaryInfo.indexInfos);
361             for (int i = 0; i < sortedIndexInfos.size(); ++i) {
362                 if (i > 0) {
363                     nameBuilder.append("-");
364                 }
365                 nameBuilder
366                 .append(IsoUtils.INSTANCE.isoCodeToLocalizedLanguageName(appContext, sortedIndexInfos.get(i).shortName));
367             }
368             name = nameBuilder.toString();
369         } else {
370             name = uncompressedFilename.replace(".quickdic", "");
371         }
372         fileToNameCache.put(uncompressedFilename, name);
373         return name;
374     }
375
376     public synchronized void moveDictionaryToTop(final DictionaryInfo dictionaryInfo) {
377         dictionaryConfig.dictionaryFilesOrdered.remove(dictionaryInfo.uncompressedFilename);
378         dictionaryConfig.dictionaryFilesOrdered.add(0, dictionaryInfo.uncompressedFilename);
379         PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, dictionaryConfig);
380     }
381
382     public synchronized void sortDictionaries() {
383         dictionaryConfig.dictionaryFilesOrdered.sort(uncompressedFilenameComparator);
384         PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, dictionaryConfig);
385     }
386
387     public synchronized void deleteDictionary(final DictionaryInfo dictionaryInfo) {
388         while (dictionaryConfig.dictionaryFilesOrdered.remove(dictionaryInfo.uncompressedFilename)) {
389         }
390         dictionaryConfig.uncompressedFilenameToDictionaryInfo
391         .remove(dictionaryInfo.uncompressedFilename);
392         getPath(dictionaryInfo.uncompressedFilename).delete();
393         PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, dictionaryConfig);
394     }
395
396     final Comparator<Object> collator = USE_COLLATOR ? CollatorWrapper.getInstance() : null;
397     final Comparator<String> uncompressedFilenameComparator = new Comparator<String>() {
398         @Override
399         public int compare(String uncompressedFilename1, String uncompressedFilename2) {
400             final String name1 = getDictionaryName(uncompressedFilename1);
401             final String name2 = getDictionaryName(uncompressedFilename2);
402             if (defaultLangName.length() > 0) {
403                 if (name1.startsWith(defaultLangName + "-")
404                         && !name2.startsWith(defaultLangName + "-")) {
405                     return -1;
406                 } else if (name2.startsWith(defaultLangName + "-")
407                         && !name1.startsWith(defaultLangName + "-")) {
408                     return 1;
409                 }
410             }
411             return collator != null ? collator.compare(name1, name2) : name1.compareToIgnoreCase(name2);
412         }
413     };
414     final Comparator<DictionaryInfo> dictionaryInfoComparator = new Comparator<DictionaryInfo>() {
415         @Override
416         public int compare(DictionaryInfo d1, DictionaryInfo d2) {
417             // Single-index dictionaries first.
418             if (d1.indexInfos.size() != d2.indexInfos.size()) {
419                 return d1.indexInfos.size() - d2.indexInfos.size();
420             }
421             return uncompressedFilenameComparator.compare(d1.uncompressedFilename,
422                     d2.uncompressedFilename);
423         }
424     };
425
426     public void backgroundUpdateDictionaries(final Runnable onUpdateFinished) {
427         new Thread(new Runnable() {
428             @Override
429             public void run() {
430                 final DictionaryConfig oldDictionaryConfig = new DictionaryConfig();
431                 synchronized (DictionaryApplication.this) {
432                     oldDictionaryConfig.dictionaryFilesOrdered
433                     .addAll(dictionaryConfig.dictionaryFilesOrdered);
434                 }
435                 final DictionaryConfig newDictionaryConfig = new DictionaryConfig();
436                 for (final String uncompressedFilename : oldDictionaryConfig.dictionaryFilesOrdered) {
437                     final File dictFile = getPath(uncompressedFilename);
438                     final DictionaryInfo dictionaryInfo = Dictionary.getDictionaryInfo(dictFile);
439                     if (dictionaryInfo.isValid() || dictFile.exists()) {
440                         newDictionaryConfig.dictionaryFilesOrdered.add(uncompressedFilename);
441                         newDictionaryConfig.uncompressedFilenameToDictionaryInfo.put(
442                             uncompressedFilename, dictionaryInfo);
443                     }
444                 }
445
446                 // Are there dictionaries on the device that we didn't know
447                 // about already?
448                 // Pick them up and put them at the end of the list.
449                 final List<String> toAddSorted = new ArrayList<>();
450                 final File[] dictDirFiles = getDictDir().listFiles();
451                 if (dictDirFiles != null) {
452                     for (final File file : dictDirFiles) {
453                         if (file.getName().endsWith(".zip")) {
454                             if (DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO
455                                     .containsKey(file.getName().replace(".zip", ""))) {
456                                 file.delete();
457                             }
458                         }
459                         if (!file.getName().endsWith(".quickdic")) {
460                             continue;
461                         }
462                         if (newDictionaryConfig.uncompressedFilenameToDictionaryInfo
463                                 .containsKey(file.getName())) {
464                             // We have it in our list already.
465                             continue;
466                         }
467                         final DictionaryInfo dictionaryInfo = Dictionary.getDictionaryInfo(file);
468                         if (!dictionaryInfo.isValid()) {
469                             Log.e(LOG, "Unable to parse dictionary: " + file.getPath());
470                         }
471
472                         toAddSorted.add(file.getName());
473                         newDictionaryConfig.uncompressedFilenameToDictionaryInfo.put(
474                             file.getName(), dictionaryInfo);
475                     }
476                 } else {
477                     Log.w(LOG, "dictDir is not a directory: " + getDictDir().getPath());
478                 }
479                 if (!toAddSorted.isEmpty()) {
480                     toAddSorted.sort(uncompressedFilenameComparator);
481                     newDictionaryConfig.dictionaryFilesOrdered.addAll(toAddSorted);
482                 }
483
484                 try {
485                     PersistentObjectCache.getInstance()
486                     .write(C.DICTIONARY_CONFIGS, newDictionaryConfig);
487                 } catch (Exception e) {
488                     Log.e(LOG, "Failed persisting dictionary configs", e);
489                 }
490
491                 synchronized (DictionaryApplication.this) {
492                     dictionaryConfig = newDictionaryConfig;
493                 }
494
495                 try {
496                     onUpdateFinished.run();
497                 } catch (Exception e) {
498                     Log.e(LOG, "Exception running callback.", e);
499                 }
500             }
501         }).start();
502     }
503
504     private boolean matchesFilters(final DictionaryInfo dictionaryInfo, final String[] filters) {
505         if (filters == null) {
506             return true;
507         }
508         for (final String filter : filters) {
509             if (!getDictionaryName(dictionaryInfo.uncompressedFilename).toLowerCase().contains(
510                         filter)) {
511                 return false;
512             }
513         }
514         return true;
515     }
516
517     public synchronized List<DictionaryInfo> getDictionariesOnDevice(String[] filters) {
518         final List<DictionaryInfo> result = new ArrayList<>(
519                 dictionaryConfig.dictionaryFilesOrdered.size());
520         for (final String uncompressedFilename : dictionaryConfig.dictionaryFilesOrdered) {
521             final DictionaryInfo dictionaryInfo = dictionaryConfig.uncompressedFilenameToDictionaryInfo
522                                                   .get(uncompressedFilename);
523             if (dictionaryInfo != null && matchesFilters(dictionaryInfo, filters)) {
524                 result.add(dictionaryInfo);
525             }
526         }
527         return result;
528     }
529
530     public List<DictionaryInfo> getDownloadableDictionaries(String[] filters) {
531         final List<DictionaryInfo> result = new ArrayList<>(
532                 dictionaryConfig.dictionaryFilesOrdered.size());
533
534         final Map<String, DictionaryInfo> remaining = new HashMap<>(
535                 DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO);
536         remaining.keySet().removeAll(dictionaryConfig.dictionaryFilesOrdered);
537         for (final DictionaryInfo dictionaryInfo : remaining.values()) {
538             if (matchesFilters(dictionaryInfo, filters)) {
539                 result.add(dictionaryInfo);
540             }
541         }
542         result.sort(dictionaryInfoComparator);
543         return result;
544     }
545
546     public boolean updateAvailable(final DictionaryInfo dictionaryInfo) {
547         final DictionaryInfo downloadable =
548             DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.get(
549                 dictionaryInfo.uncompressedFilename);
550         return downloadable != null &&
551                downloadable.creationMillis > dictionaryInfo.creationMillis;
552     }
553
554     public DictionaryInfo getDownloadable(final String uncompressedFilename) {
555         return DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.get(uncompressedFilename);
556     }
557
558 }