]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryApplication.java
Using external storage dir properly, better notifications when missing.
[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.app.Application;
18 import android.content.Context;
19 import android.content.Intent;
20 import android.content.SharedPreferences;
21 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
22 import android.net.Uri;
23 import android.os.Environment;
24 import android.preference.PreferenceManager;
25 import android.util.Log;
26 import android.util.TypedValue;
27 import android.view.View;
28 import android.view.View.OnClickListener;
29 import android.view.ViewGroup.LayoutParams;
30 import android.widget.Button;
31 import android.widget.ImageButton;
32 import android.widget.ImageView.ScaleType;
33 import android.widget.LinearLayout;
34
35 import com.actionbarsherlock.view.Menu;
36 import com.actionbarsherlock.view.MenuItem;
37 import com.actionbarsherlock.view.MenuItem.OnMenuItemClickListener;
38 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;
39 import com.hughes.android.dictionary.engine.Dictionary;
40 import com.hughes.android.dictionary.engine.Language;
41 import com.hughes.android.dictionary.engine.TransliteratorManager;
42 import com.hughes.android.dictionary.engine.Language.LanguageResources;
43 import com.hughes.android.util.IntentLauncher;
44 import com.hughes.android.util.PersistentObjectCache;
45 import com.hughes.util.ListUtil;
46 import com.ibm.icu.text.Collator;
47
48 import java.io.BufferedReader;
49 import java.io.File;
50 import java.io.IOException;
51 import java.io.InputStreamReader;
52 import java.io.Serializable;
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.Comparator;
56 import java.util.LinkedHashMap;
57 import java.util.List;
58 import java.util.Locale;
59 import java.util.Map;
60
61 public class DictionaryApplication extends Application {
62   
63   static final String LOG = "QuickDicApp";
64   
65   // Static, determined by resources (and locale).
66   // Unordered.
67   static Map<String,DictionaryInfo> DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO = null;
68   
69   static final class DictionaryConfig implements Serializable {
70     private static final long serialVersionUID = -1444177164708201263L;
71     // User-ordered list, persisted, just the ones that are/have been present.
72     final List<String> dictionaryFilesOrdered = new ArrayList<String>();
73     
74     final Map<String, DictionaryInfo> uncompressedFilenameToDictionaryInfo = new LinkedHashMap<String, DictionaryInfo>();
75   }
76   DictionaryConfig dictionaryConfig = null;
77   
78   
79   int languageButtonPixels = 22;
80
81 //  static final class DictionaryHistory implements Serializable {
82 //    private static final long serialVersionUID = -4842995032541390284L;
83 //    // User-ordered list, persisted, just the ones that are/have been present.
84 //    final List<DictionaryLink> dictionaryLinks = new ArrayList<DictionaryLink>();
85 //  }
86 //  DictionaryHistory dictionaryHistory = null;
87   
88   static synchronized void staticInit(final Context context) {
89     if (DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO != null) {
90       return;
91     }
92     DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO = new LinkedHashMap<String,DictionaryInfo>();
93     final BufferedReader reader = new BufferedReader(
94             new InputStreamReader(context.getResources().openRawResource(R.raw.dictionary_info)));
95     try {
96       String line;
97       while ((line = reader.readLine()) != null) {
98         if (line.startsWith("#") || line.length() == 0) {
99           continue;
100         }
101         final DictionaryInfo dictionaryInfo = new DictionaryInfo(line);
102         DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.put(
103                 dictionaryInfo.uncompressedFilename, dictionaryInfo);
104       }
105       reader.close();
106     } catch (IOException e) {
107       Log.e(LOG, "Failed to load downloadable dictionary lists.", e);
108     }
109   }
110
111   
112   private File dictDir;
113
114   @Override
115   public void onCreate() {
116     super.onCreate();
117     Log.d("QuickDic", "Application: onCreate");
118     TransliteratorManager.init(null);
119     staticInit(getApplicationContext());
120     
121     languageButtonPixels = (int) TypedValue.applyDimension(
122             TypedValue.COMPLEX_UNIT_DIP, 60, getResources().getDisplayMetrics());
123     
124     // Load the dictionaries we know about.
125     dictionaryConfig = PersistentObjectCache.init(getApplicationContext()).read(
126             C.DICTIONARY_CONFIGS, DictionaryConfig.class);
127     if (dictionaryConfig == null) {
128       dictionaryConfig = new DictionaryConfig();
129     }
130     
131     // Theme stuff.
132     setTheme(getSelectedTheme().themeId);
133     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
134     prefs.registerOnSharedPreferenceChangeListener(new OnSharedPreferenceChangeListener() {
135       @Override
136       public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
137           String key) {
138         Log.d("QuickDic", "prefs changed: " + key);
139         if (key.equals(getString(R.string.themeKey))) {
140           setTheme(getSelectedTheme().themeId);
141         }
142       }
143     });
144   }
145   
146   public void onCreateGlobalOptionsMenu(
147       final Context context, final Menu menu) {
148     final MenuItem about = menu.add(getString(R.string.about));
149     about.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
150     about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
151       public boolean onMenuItemClick(final MenuItem menuItem) {
152         final Intent intent = new Intent().setClassName(AboutActivity.class
153             .getPackage().getName(), AboutActivity.class.getCanonicalName());
154         context.startActivity(intent);
155         return false;
156       }
157     });
158
159     final MenuItem help = menu.add(getString(R.string.help));
160     help.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
161     help.setOnMenuItemClickListener(new OnMenuItemClickListener() {
162       public boolean onMenuItemClick(final MenuItem menuItem) {
163         context.startActivity(HtmlDisplayActivity.getHelpLaunchIntent());
164         return false;
165       }
166     });
167
168     final MenuItem preferences = menu.add(getString(R.string.settings));
169     preferences.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
170     preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
171       public boolean onMenuItemClick(final MenuItem menuItem) {
172         PreferenceActivity.prefsMightHaveChanged = true;
173         final Intent intent = new Intent().setClassName(PreferenceActivity.class
174             .getPackage().getName(), PreferenceActivity.class.getCanonicalName());
175         context.startActivity(intent);
176         return false;
177       }
178     });
179     
180     
181     final MenuItem reportIssue = menu.add(getString(R.string.reportIssue));
182     reportIssue.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
183     reportIssue.setOnMenuItemClickListener(new OnMenuItemClickListener() {
184       public boolean onMenuItemClick(final MenuItem menuItem) {
185         final Intent intent = new Intent(Intent.ACTION_VIEW);
186         intent.setData(Uri.parse("http://code.google.com/p/quickdic-dictionary/issues/entry"));
187         context.startActivity(intent);
188         return false;
189       }
190     });
191   }
192   
193   public synchronized File getDictDir() {
194     // This metaphor doesn't work, because we've already reset prefsMightHaveChanged.
195 //    if (dictDir == null || PreferenceActivity.prefsMightHaveChanged) {
196       final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
197       final File defaultDictDir = new File(Environment.getExternalStorageDirectory(), "quickDic");
198       String dir = prefs.getString(getString(R.string.quickdicDirectoryKey), defaultDictDir.getAbsolutePath());
199       if (dir.isEmpty()) {
200           dir = defaultDictDir.getAbsolutePath();
201       }
202       dictDir = new File(dir);
203       dictDir.mkdirs();
204       return dictDir;
205   }
206   
207   public C.Theme getSelectedTheme() {
208     final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
209     final String theme = prefs.getString(getString(R.string.themeKey), "themeLight");
210     if (theme.equals("themeLight")) {
211       return C.Theme.LIGHT;
212     } else {
213       return C.Theme.DEFAULT;
214     }
215   }
216     
217   public File getPath(String uncompressedFilename) {
218     return new File(getDictDir(), uncompressedFilename);
219   }
220   
221
222   String defaultLangISO2 = Locale.getDefault().getLanguage().toLowerCase();
223   String defaultLangName = null;
224   final Map<String, String> fileToNameCache = new LinkedHashMap<String, String>();
225
226   public String isoCodeToLocalizedLanguageName(final String isoCode) {
227     final Language.LanguageResources languageResources = Language.isoCodeToResources.get(isoCode); 
228     final String lang = languageResources != null ? getApplicationContext().getString(languageResources.nameId) : isoCode;
229     return lang;
230   }
231   
232   public List<IndexInfo> sortedIndexInfos(List<IndexInfo> indexInfos) {
233       // Hack to put the default locale first in the name.
234       if (indexInfos.size() > 1 && 
235           indexInfos.get(1).shortName.toLowerCase().equals(defaultLangISO2)) {
236         List<IndexInfo> result = new ArrayList<DictionaryInfo.IndexInfo>(indexInfos);
237         ListUtil.swap(result, 0, 1);
238         return result;
239       }
240       return indexInfos;
241   }
242   
243
244   public synchronized String getDictionaryName(final String uncompressedFilename) {
245     final String currentLocale = Locale.getDefault().getLanguage().toLowerCase(); 
246     if (!currentLocale.equals(defaultLangISO2)) {
247       defaultLangISO2 = currentLocale;
248       fileToNameCache.clear();
249       defaultLangName = null;
250     }
251     if (defaultLangName == null) {
252       defaultLangName = isoCodeToLocalizedLanguageName(defaultLangISO2);
253     }
254     
255     String name = fileToNameCache.get(uncompressedFilename);
256     if (name != null) {
257       return name;
258     }
259     
260     final DictionaryInfo dictionaryInfo = DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.get(uncompressedFilename);
261     if (dictionaryInfo != null) {
262       final StringBuilder nameBuilder = new StringBuilder();
263
264       List<IndexInfo> sortedIndexInfos = sortedIndexInfos(dictionaryInfo.indexInfos);
265       for (int i = 0; i < sortedIndexInfos.size(); ++i) {
266         if (i > 0) {
267           nameBuilder.append("-");
268         }
269         nameBuilder.append(isoCodeToLocalizedLanguageName(sortedIndexInfos.get(i).shortName));
270       }
271       name = nameBuilder.toString();
272     } else {
273       name = uncompressedFilename.replace(".quickdic", "");
274     }
275     fileToNameCache.put(uncompressedFilename, name);
276     return name;
277   }
278   
279   public View createButton(final Context context, final DictionaryInfo dictionaryInfo,
280           final IndexInfo indexInfo) {
281       LanguageResources languageResources = Language.isoCodeToResources.get(indexInfo.shortName);
282       View result;
283             
284       if (languageResources == null || languageResources.flagId <= 0) {
285           Button button = new Button(context);
286           button.setText(indexInfo.shortName);
287           result = button;
288       } else {
289           ImageButton button = new ImageButton(context);
290           button.setImageResource(languageResources.flagId);
291           button.setScaleType(ScaleType.FIT_CENTER);
292           result = button;
293       }
294       result.setMinimumWidth(languageButtonPixels);
295       result.setMinimumHeight(languageButtonPixels * 2 / 3);
296 //      result.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
297       return result;
298   }
299
300
301   public synchronized void moveDictionaryToTop(final DictionaryInfo dictionaryInfo) {
302     dictionaryConfig.dictionaryFilesOrdered.remove(dictionaryInfo.uncompressedFilename);
303     dictionaryConfig.dictionaryFilesOrdered.add(0, dictionaryInfo.uncompressedFilename);
304     PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, dictionaryConfig);
305   }
306
307   public synchronized void deleteDictionary(final DictionaryInfo dictionaryInfo) {
308     while (dictionaryConfig.dictionaryFilesOrdered.remove(dictionaryInfo.uncompressedFilename)) {};
309     dictionaryConfig.uncompressedFilenameToDictionaryInfo.remove(dictionaryInfo.uncompressedFilename);
310     getPath(dictionaryInfo.uncompressedFilename).delete();
311     PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, dictionaryConfig);
312   }
313
314   final Collator collator = Collator.getInstance();
315   final Comparator<String> uncompressedFilenameComparator = new Comparator<String>() {
316     @Override
317     public int compare(String uncompressedFilename1, String uncompressedFilename2) {
318       final String name1 = getDictionaryName(uncompressedFilename1);
319       final String name2 = getDictionaryName(uncompressedFilename2);
320       if (defaultLangName.length() > 0) {
321         if (name1.startsWith(defaultLangName + "-") && !name2.startsWith(defaultLangName + "-")) {
322           return -1;
323         } else if (name2.startsWith(defaultLangName + "-") && !name1.startsWith(defaultLangName + "-")) {
324           return 1;
325         }
326       }
327       return collator.compare(name1, name2);
328     }
329   };
330   final Comparator<DictionaryInfo> dictionaryInfoComparator = new Comparator<DictionaryInfo>() {
331     @Override
332     public int compare(DictionaryInfo d1, DictionaryInfo d2) {
333       // Single-index dictionaries first.
334       if (d1.indexInfos.size() != d2.indexInfos.size()) {
335           return d1.indexInfos.size() - d2.indexInfos.size();
336       }
337       return uncompressedFilenameComparator.compare(d1.uncompressedFilename, d2.uncompressedFilename);
338     }
339   };
340   
341   public void backgroundUpdateDictionaries(final Runnable onUpdateFinished) {
342     new Thread(new Runnable() {
343       @Override
344       public void run() {
345         final DictionaryConfig oldDictionaryConfig = new DictionaryConfig();
346         synchronized(this) {
347           oldDictionaryConfig.dictionaryFilesOrdered.addAll(dictionaryConfig.dictionaryFilesOrdered);
348         }
349         final DictionaryConfig newDictionaryConfig = new DictionaryConfig();
350         for (final String uncompressedFilename : oldDictionaryConfig.dictionaryFilesOrdered) {
351           final File dictFile = getPath(uncompressedFilename);
352           final DictionaryInfo dictionaryInfo = Dictionary.getDictionaryInfo(dictFile);
353           if (dictionaryInfo != null) {
354             newDictionaryConfig.dictionaryFilesOrdered.add(uncompressedFilename);
355             newDictionaryConfig.uncompressedFilenameToDictionaryInfo.put(uncompressedFilename, dictionaryInfo);
356           }
357         }
358         
359         // Are there dictionaries on the device that we didn't know about already?
360         // Pick them up and put them at the end of the list.
361         final List<String> toAddSorted = new ArrayList<String>();
362         final File[] dictDirFiles = getDictDir().listFiles();
363         if (dictDirFiles != null) {
364           for (final File file : dictDirFiles) {
365             if (file.getName().endsWith(".zip")) {
366               if (DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.containsKey(file.getName().replace(".zip", ""))) {
367                 file.delete();
368               }
369             }
370             if (!file.getName().endsWith(".quickdic")) {
371               continue;
372             }
373             if (newDictionaryConfig.uncompressedFilenameToDictionaryInfo.containsKey(file.getName())) {
374               // We have it in our list already.
375               continue;
376             }
377             final DictionaryInfo dictionaryInfo = Dictionary.getDictionaryInfo(file);
378             if (dictionaryInfo == null) {
379               Log.e(LOG, "Unable to parse dictionary: " + file.getPath());
380               continue;
381             }
382             
383             toAddSorted.add(file.getName());
384             newDictionaryConfig.uncompressedFilenameToDictionaryInfo.put(file.getName(), dictionaryInfo);
385           }
386         } else {
387           Log.w(LOG, "dictDir is not a diretory: " + getDictDir().getPath());
388         }
389         if (!toAddSorted.isEmpty()) {
390           Collections.sort(toAddSorted, uncompressedFilenameComparator);
391           newDictionaryConfig.dictionaryFilesOrdered.addAll(toAddSorted);
392         }
393
394         PersistentObjectCache.getInstance().write(C.DICTIONARY_CONFIGS, newDictionaryConfig);
395         synchronized (this) {
396           dictionaryConfig = newDictionaryConfig;
397         }
398         
399         try {
400           onUpdateFinished.run();
401         } catch (Exception e) {
402           Log.e(LOG, "Exception running callback.", e);
403         }
404       }}).start();
405   }
406   
407   public boolean matchesFilters(final DictionaryInfo dictionaryInfo, final String[] filters) {
408       if (filters == null) {
409           return true;
410       }
411       for (final String filter : filters) {
412           if (!getDictionaryName(dictionaryInfo.uncompressedFilename).toLowerCase().contains(filter)) {
413               return false;
414           }
415       }
416       return true;
417   }
418
419   public synchronized List<DictionaryInfo> getDictionariesOnDevice(String[] filters) {
420     final List<DictionaryInfo> result = new ArrayList<DictionaryInfo>(dictionaryConfig.dictionaryFilesOrdered.size());
421     for (final String uncompressedFilename : dictionaryConfig.dictionaryFilesOrdered) {
422       final DictionaryInfo dictionaryInfo = dictionaryConfig.uncompressedFilenameToDictionaryInfo.get(uncompressedFilename);
423       if (dictionaryInfo != null && matchesFilters(dictionaryInfo, filters)) {
424         result.add(dictionaryInfo);
425       }
426     }
427     return result;
428   }
429   
430   public List<DictionaryInfo> getDownloadableDictionaries(String[] filters) {
431       final List<DictionaryInfo> result = new ArrayList<DictionaryInfo>(dictionaryConfig.dictionaryFilesOrdered.size());
432       
433       final Map<String, DictionaryInfo> remaining = new LinkedHashMap<String, DictionaryInfo>(DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO);
434       remaining.keySet().removeAll(dictionaryConfig.dictionaryFilesOrdered);
435       for (final DictionaryInfo dictionaryInfo : remaining.values()) {
436           if (matchesFilters(dictionaryInfo, filters)) {
437               result.add(dictionaryInfo);
438           }
439       }
440       Collections.sort(result, dictionaryInfoComparator);
441       return result;
442   }
443   
444   public synchronized boolean isDictionaryOnDevice(String uncompressedFilename) {
445     return dictionaryConfig.uncompressedFilenameToDictionaryInfo.get(uncompressedFilename) != null;
446   }
447
448   public boolean updateAvailable(final DictionaryInfo dictionaryInfo) {
449     final DictionaryInfo downloadable = 
450             DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.get(
451                     dictionaryInfo.uncompressedFilename);
452     return downloadable != null && 
453             downloadable.creationMillis > dictionaryInfo.creationMillis;
454   }
455
456   public DictionaryInfo getDownloadable(final String uncompressedFilename) {
457     final DictionaryInfo downloadable = DOWNLOADABLE_UNCOMPRESSED_FILENAME_NAME_TO_DICTIONARY_INFO.get(uncompressedFilename);
458     return downloadable;
459   }
460
461 }