1 | /* This file is part of Aard Dictionary for Android <http://aarddict.org>. |
2 | * |
3 | * This program is free software: you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License version 3 |
5 | * as published by the Free Software Foundation. |
6 | * |
7 | * This program is distributed in the hope that it will be useful, |
8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | * GNU General Public License <http://www.gnu.org/licenses/gpl-3.0.txt> |
11 | * for more details. |
12 | * |
13 | * Copyright (C) 2010 Igor Tkach |
14 | */ |
15 | |
16 | package aarddict.android; |
17 | |
18 | import java.io.File; |
19 | import java.io.FileInputStream; |
20 | import java.io.FileOutputStream; |
21 | import java.io.FilenameFilter; |
22 | import java.io.IOException; |
23 | import java.io.ObjectInputStream; |
24 | import java.io.ObjectOutputStream; |
25 | import java.util.ArrayList; |
26 | import java.util.Arrays; |
27 | import java.util.Collections; |
28 | import java.util.HashMap; |
29 | import java.util.HashSet; |
30 | import java.util.Iterator; |
31 | import java.util.LinkedHashMap; |
32 | import java.util.LinkedHashSet; |
33 | import java.util.List; |
34 | import java.util.Map; |
35 | import java.util.Set; |
36 | import java.util.UUID; |
37 | |
38 | import aarddict.Article; |
39 | import aarddict.ArticleNotFound; |
40 | import aarddict.Entry; |
41 | import aarddict.Header; |
42 | import aarddict.Library; |
43 | import aarddict.Metadata; |
44 | import aarddict.RedirectTooManyLevels; |
45 | import aarddict.Volume; |
46 | import android.app.Service; |
47 | import android.content.BroadcastReceiver; |
48 | import android.content.Context; |
49 | import android.content.Intent; |
50 | import android.content.IntentFilter; |
51 | import android.net.Uri; |
52 | import android.os.Binder; |
53 | import android.os.FileObserver; |
54 | import android.os.IBinder; |
55 | import android.util.Log; |
56 | |
57 | public final class DictionaryService extends Service { |
58 | |
59 | public class LocalBinder extends Binder { |
60 | DictionaryService getService() { |
61 | return DictionaryService.this; |
62 | } |
63 | } |
64 | |
65 | private final static String TAG = "aarddict.android.DictionaryService"; |
66 | |
67 | public final static String DISCOVERY_STARTED = TAG + ".DISCOVERY_STARTED"; |
68 | public final static String DISCOVERY_FINISHED = TAG + ".DISCOVERY_FINISHED"; |
69 | public final static String OPEN_STARTED = TAG + ".OPEN_STARTED"; |
70 | public final static String OPENED_DICT = TAG + ".OPENED_DICT"; |
71 | public final static String CLOSED_DICT = TAG + ".CLOSED_DICT"; |
72 | public final static String DICT_OPEN_FAILED = TAG + ".DICT_OPEN_FAILED"; |
73 | public final static String OPEN_FINISHED = TAG + ".OPEN_FINISHED"; |
74 | |
75 | private Library library; |
76 | |
77 | private Set<String> excludedScanDirs = new HashSet<String>() { |
78 | { |
79 | add("/proc"); |
80 | add("/dev"); |
81 | add("/etc"); |
82 | add("/sys"); |
83 | add("/acct"); |
84 | add("/cache"); |
85 | } |
86 | }; |
87 | |
88 | private FilenameFilter fileFilter = new FilenameFilter() { |
89 | public boolean accept(File dir, String filename) { |
90 | return filename.toLowerCase().endsWith( |
91 | ".aar") || new File(dir, filename).isDirectory(); |
92 | } |
93 | }; |
94 | |
95 | @Override |
96 | public IBinder onBind(Intent intent) { |
97 | return binder; |
98 | } |
99 | |
100 | private final IBinder binder = new LocalBinder(); |
101 | |
102 | private BroadcastReceiver broadcastReceiver; |
103 | |
104 | @Override |
105 | public void onCreate() { |
106 | Log.d(TAG, "On create"); |
107 | library = new Library(); |
108 | loadDictFileList(); |
109 | broadcastReceiver = new BroadcastReceiver() { |
110 | @Override |
111 | public void onReceive(Context context, Intent intent) { |
112 | String action = intent.getAction(); |
113 | Uri path = intent.getData(); |
114 | Log.d(TAG, String.format("action: %s, path: %s", action, path)); |
115 | stopSelf(); |
116 | } |
117 | }; |
118 | IntentFilter intentFilter = new IntentFilter(); |
119 | intentFilter.addDataScheme("file"); |
120 | intentFilter.addAction(Intent.ACTION_MEDIA_EJECT); |
121 | intentFilter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); |
122 | intentFilter.addAction(Intent.ACTION_MEDIA_REMOVED); |
123 | registerReceiver(broadcastReceiver, intentFilter); |
124 | } |
125 | |
126 | @Override |
127 | public void onStart(Intent intent, int startId) { |
128 | String action = intent == null ? null : intent.getAction(); |
129 | if (action != null && action.equals(Intent.ACTION_VIEW)) { |
130 | final Uri data = intent.getData(); |
131 | Log.d(TAG, "Path: " + data.getPath()); |
132 | if (data != null && data.getPath() != null) { |
133 | Runnable r = new Runnable() { |
134 | public void run() { |
135 | Log.d(TAG, "opening: " + data.getPath()); |
136 | open(new File(data.getPath())); |
137 | } |
138 | }; |
139 | new Thread(r).start(); |
140 | } |
141 | } |
142 | } |
143 | |
144 | synchronized public void openDictionaries() { |
145 | Log.d(TAG, "opening dictionaries"); |
146 | long t0 = System.currentTimeMillis(); |
147 | List<File> candidates = new ArrayList<File>(); |
148 | for (String path : dictionaryFileNames) { |
149 | candidates.add(new File(path)); |
150 | } |
151 | open(candidates); |
152 | Log.d(TAG, "dictionaries opened in " + (System.currentTimeMillis() - t0)); |
153 | } |
154 | |
155 | |
156 | synchronized public void refresh() { |
157 | Log.d(TAG, "starting dictionary discovery"); |
158 | long t0 = System.currentTimeMillis(); |
159 | List<File> candidates = discover(); |
160 | Map<File, Exception> errors = open(candidates); |
161 | for (File file : candidates) { |
162 | String absolutePath = file.getAbsolutePath(); |
163 | if (!errors.containsKey(file)) { |
164 | dictionaryFileNames.add(absolutePath); |
165 | } |
166 | else { |
167 | Log.w(TAG, "Failed to open file " + absolutePath, errors.get(file)); |
168 | } |
169 | } |
170 | saveDictFileList(); |
171 | Log.d(TAG, "dictionary discovery took " + (System.currentTimeMillis() - t0)); |
172 | } |
173 | |
174 | private Set<String> dictionaryFileNames = new LinkedHashSet<String>(); |
175 | |
176 | synchronized public Map<File, Exception> open(File file) { |
177 | Map<File, Exception> errors = open(Arrays.asList(new File[]{file})); |
178 | if (errors.size() == 0 && |
179 | !dictionaryFileNames.contains(file.getAbsoluteFile())) { |
180 | saveDictFileList(); |
181 | } |
182 | return errors; |
183 | } |
184 | |
185 | private final class DeleteObserver extends FileObserver { |
186 | |
187 | private Set<String> dictFilesToWatch; |
188 | private String dir; |
189 | |
190 | DeleteObserver(String dir) { |
191 | super(dir, DELETE); |
192 | dictFilesToWatch = new HashSet<String>(); |
193 | this.dir = dir; |
194 | } |
195 | |
196 | public void add(String pathToWatch) { |
197 | Log.d(TAG, String.format("Watch file %s in %s", pathToWatch, dir)); |
198 | dictFilesToWatch.add(pathToWatch); |
199 | } |
200 | |
201 | @Override |
202 | public void onEvent(int event, String path) { |
203 | if ((event & FileObserver.DELETE) != 0) { |
204 | Log.d(TAG, String.format("Received file event %s: %s", event, path)); |
205 | if (dictFilesToWatch.contains(path)) { |
206 | Log.d(TAG, String.format("Dictionary file %s in %s has been deleted, stopping service", path, dir)); |
207 | if (dictionaryFileNames.remove(new File(dir, path).getAbsolutePath())) |
208 | saveDictFileList(); |
209 | stopSelf(); |
210 | } |
211 | } |
212 | } |
213 | |
214 | } |
215 | |
216 | private Map<String, DeleteObserver> deleteObservers = new HashMap<String, DeleteObserver>(); |
217 | |
218 | private DeleteObserver getDeleteObserver(File file) { |
219 | File parent = file.getParentFile(); |
220 | String dir = parent.getAbsolutePath(); |
221 | DeleteObserver observer = deleteObservers.get(dir); |
222 | if (observer == null) { |
223 | observer = new DeleteObserver(dir); |
224 | observer.startWatching(); |
225 | deleteObservers.put(dir, observer); |
226 | } |
227 | return observer; |
228 | } |
229 | |
230 | synchronized private Map<File, Exception> open(List<File> files) { |
231 | Map<File, Exception> errors = new HashMap<File, Exception>(); |
232 | if (files.size() == 0) { |
233 | return errors; |
234 | } |
235 | Intent notifyOpenStarted = new Intent(OPEN_STARTED); |
236 | notifyOpenStarted.putExtra("count", files.size()); |
237 | sendBroadcast(notifyOpenStarted); |
238 | Thread.yield(); |
239 | |
240 | File cacheDir = getCacheDir(); |
241 | File metaCacheDir = new File(cacheDir, "metadata"); |
242 | if (!metaCacheDir.exists()) { |
243 | if (!metaCacheDir.mkdir()) { |
244 | Log.w(TAG, "Failed to create metadata cache directory"); |
245 | } |
246 | } |
247 | |
248 | Map<UUID, Metadata> knownMeta = new HashMap<UUID, Metadata>(); |
249 | for (int i = 0; i < files.size(); i++) { |
250 | File file = files.get(i); |
251 | Volume d = null; |
252 | try { |
253 | Log.d(TAG, "Opening " + file.getName()); |
254 | d = new Volume(file, metaCacheDir, knownMeta); |
255 | Volume existing = library.getVolume(d.getId()); |
256 | if (existing == null) { |
257 | Log.d(TAG, "Dictionary " + d.getId() + " is not in current collection"); |
258 | library.add(d); |
259 | DeleteObserver observer = getDeleteObserver(file); |
260 | observer.add(file.getName()); |
261 | } |
262 | else { |
263 | Log.d(TAG, "Dictionary " + d.getId() + " is already open"); |
264 | } |
265 | Intent notifyOpened = new Intent(OPENED_DICT); |
266 | notifyOpened.putExtra("title", d.getDisplayTitle()); |
267 | notifyOpened.putExtra("count", files.size()); |
268 | notifyOpened.putExtra("i", i); |
269 | sendBroadcast(notifyOpened); |
270 | Thread.yield(); |
271 | } |
272 | catch (Exception e) { |
273 | Log.e(TAG, "Failed to open " + file, e); |
274 | Intent notifyFailed = new Intent(DICT_OPEN_FAILED); |
275 | notifyFailed.putExtra("file", file.getAbsolutePath()); |
276 | notifyFailed.putExtra("count", files.size()); |
277 | notifyFailed.putExtra("i", i); |
278 | sendBroadcast(notifyFailed); |
279 | Thread.yield(); |
280 | errors.put(file, e); |
281 | } |
282 | } |
283 | sendBroadcast(new Intent(OPEN_FINISHED)); |
284 | Thread.yield(); |
285 | return errors; |
286 | } |
287 | |
288 | @Override |
289 | public void onDestroy() { |
290 | super.onDestroy(); |
291 | unregisterReceiver(broadcastReceiver); |
292 | for (Volume d : library) { |
293 | try { |
294 | d.close(); |
295 | } |
296 | catch (IOException e) { |
297 | Log.e(TAG, "Failed to close " + d, e); |
298 | } |
299 | } |
300 | library.clear(); |
301 | for (DeleteObserver observer : deleteObservers.values()) { |
302 | observer.stopWatching(); |
303 | } |
304 | Log.i(TAG, "destroyed"); |
305 | } |
306 | |
307 | public List<File> discover() { |
308 | sendBroadcast(new Intent(DISCOVERY_STARTED)); |
309 | Thread.yield(); |
310 | File scanRoot = new File ("/"); |
311 | List<File> result = new ArrayList<File>(); |
312 | result.addAll(scanDir(scanRoot)); |
313 | Intent intent = new Intent(DISCOVERY_FINISHED); |
314 | intent.putExtra("count", result.size()); |
315 | sendBroadcast(intent); |
316 | Thread.yield(); |
317 | return result; |
318 | } |
319 | |
320 | private List<File> scanDir(File dir) { |
321 | String absolutePath = dir.getAbsolutePath(); |
322 | if (excludedScanDirs.contains(absolutePath)) { |
323 | Log.d(TAG, String.format("%s is excluded", absolutePath)); |
324 | return Collections.EMPTY_LIST; |
325 | } |
326 | boolean symlink = false; |
327 | try { |
328 | symlink = isSymlink(dir); |
329 | } catch (IOException e) { |
330 | Log.e(TAG, |
331 | String.format("Failed to check if %s is symlink", |
332 | dir.getAbsolutePath())); |
333 | } |
334 | |
335 | if (symlink) { |
336 | Log.d(TAG, String.format("%s is a symlink", absolutePath)); |
337 | return Collections.EMPTY_LIST; |
338 | } |
339 | |
340 | if (dir.isHidden()) { |
341 | Log.d(TAG, String.format("%s is hidden", absolutePath)); |
342 | return Collections.EMPTY_LIST; |
343 | } |
344 | Log.d(TAG, "Scanning " + absolutePath); |
345 | List<File> candidates = new ArrayList<File>(); |
346 | File[] files = dir.listFiles(fileFilter); |
347 | if (files != null) { |
348 | for (int i = 0; i < files.length; i++) { |
349 | File file = files[i]; |
350 | if (file.isDirectory()) { |
351 | candidates.addAll(scanDir(file)); |
352 | } else { |
353 | if (!file.isHidden() && file.isFile()) { |
354 | candidates.add(file); |
355 | } |
356 | } |
357 | } |
358 | } |
359 | return candidates; |
360 | } |
361 | |
362 | static boolean isSymlink(File file) throws IOException { |
363 | File fileInCanonicalDir = null; |
364 | if (file.getParent() == null) { |
365 | fileInCanonicalDir = file; |
366 | } else { |
367 | File canonicalDir = file.getParentFile().getCanonicalFile(); |
368 | fileInCanonicalDir = new File(canonicalDir, file.getName()); |
369 | } |
370 | if (fileInCanonicalDir.getCanonicalFile().equals( |
371 | fileInCanonicalDir.getAbsoluteFile())) { |
372 | return false; |
373 | } else { |
374 | return true; |
375 | } |
376 | } |
377 | |
378 | public void setPreferred(String volumeId) { |
379 | library.makeFirst(volumeId); |
380 | } |
381 | |
382 | public Iterator<Entry> lookup(CharSequence word) { |
383 | return library.bestMatch(word.toString()); |
384 | } |
385 | |
386 | public Iterator<Entry> followLink(CharSequence word, String fromVolumeId) throws ArticleNotFound { |
387 | return library.followLink(word.toString(), fromVolumeId); |
388 | } |
389 | |
390 | public Article redirect(Article article) throws RedirectTooManyLevels, ArticleNotFound, IOException { |
391 | return library.redirect(article); |
392 | } |
393 | |
394 | public Volume getVolume(String volumeId) { |
395 | return library.getVolume(volumeId); |
396 | } |
397 | |
398 | public Library getDictionaries() { |
399 | return library; |
400 | } |
401 | |
402 | public CharSequence getDisplayTitle(String volumeId) { |
403 | return library.getVolume(volumeId).getDisplayTitle(); |
404 | } |
405 | |
406 | @SuppressWarnings("unchecked") |
407 | public Map<UUID, List<Volume>> getVolumes() { |
408 | Map<UUID, List<Volume>> result = new LinkedHashMap(); |
409 | for (Volume d : library) { |
410 | UUID dictionaryId = d.getDictionaryId(); |
411 | if (!result.containsKey(dictionaryId)) { |
412 | result.put(dictionaryId, new ArrayList<Volume>()); |
413 | } |
414 | result.get(dictionaryId).add(d); |
415 | } |
416 | return result; |
417 | } |
418 | |
419 | |
420 | public Article getArticle(Entry entry) throws IOException { |
421 | return library.getArticle(entry); |
422 | } |
423 | |
424 | void saveDictFileList() { |
425 | try { |
426 | File dir = getDir(DICTDIR, 0); |
427 | File file = new File(dir, DICTFILE); |
428 | FileOutputStream fout = new FileOutputStream(file); |
429 | ObjectOutputStream oout = new ObjectOutputStream(fout); |
430 | oout.writeObject(new ArrayList<String>(dictionaryFileNames)); |
431 | } |
432 | catch (Exception e) { |
433 | Log.e(TAG, "Failed to save dictionary file list", e); |
434 | } |
435 | } |
436 | |
437 | private final static String DICTDIR = "dicts"; |
438 | private final static String DICTFILE = "dicts.list"; |
439 | |
440 | @SuppressWarnings("unchecked") |
441 | void loadDictFileList() { |
442 | try { |
443 | File dir = getDir(DICTDIR, 0); |
444 | File file = new File(dir, DICTFILE); |
445 | if (file.exists()) { |
446 | FileInputStream fin = new FileInputStream(file); |
447 | ObjectInputStream oin = new ObjectInputStream(fin); |
448 | List<String> data = (List<String>)oin.readObject(); |
449 | dictionaryFileNames.addAll(data); |
450 | } |
451 | } |
452 | catch (Exception e) { |
453 | Log.e(TAG, "Failed to load dictionary file list", e); |
454 | } |
455 | } |
456 | } |