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.IOException; |
22 | import java.io.ObjectInputStream; |
23 | import java.io.ObjectOutputStream; |
24 | import java.util.ArrayList; |
25 | import java.util.Date; |
26 | import java.util.HashMap; |
27 | import java.util.List; |
28 | import java.util.Map; |
29 | import java.util.Timer; |
30 | import java.util.TimerTask; |
31 | import java.util.UUID; |
32 | |
33 | import aarddict.VerifyProgressListener; |
34 | import aarddict.Volume; |
35 | import android.app.AlertDialog; |
36 | import android.app.ProgressDialog; |
37 | import android.content.Context; |
38 | import android.content.DialogInterface; |
39 | import android.content.Intent; |
40 | import android.content.DialogInterface.OnCancelListener; |
41 | import android.content.DialogInterface.OnClickListener; |
42 | import android.content.res.Resources; |
43 | import android.net.Uri; |
44 | import android.text.Html; |
45 | import android.text.format.DateUtils; |
46 | import android.text.method.LinkMovementMethod; |
47 | import android.util.Log; |
48 | import android.view.LayoutInflater; |
49 | import android.view.Menu; |
50 | import android.view.MenuItem; |
51 | import android.view.View; |
52 | import android.view.ViewGroup; |
53 | import android.widget.AdapterView; |
54 | import android.widget.BaseAdapter; |
55 | import android.widget.Button; |
56 | import android.widget.ListView; |
57 | import android.widget.TextView; |
58 | import android.widget.Toast; |
59 | import android.widget.TwoLineListItem; |
60 | |
61 | public final class DictionariesActivity extends BaseDictionaryActivity { |
62 | |
63 | private final static String TAG = DictionariesActivity.class.getName(); |
64 | |
65 | private ListView listView; |
66 | private Map<UUID, VerifyRecord> verifyData = new HashMap<UUID, VerifyRecord>(); |
67 | private DictListAdapter dataAdapter; |
68 | |
69 | private boolean aboutToFinish = false; |
70 | |
71 | @Override |
72 | void onDictionaryServiceConnected() { |
73 | Intent intent = getIntent(); |
74 | String action = intent.getAction(); |
75 | |
76 | if (action != null && action.equals(Intent.ACTION_VIEW)) { |
77 | final Uri data = intent.getData(); |
78 | Log.d(TAG, "Path: " + data.getPath()); |
79 | if (data != null && data.getPath() != null) { |
80 | Runnable r = new Runnable() { |
81 | public void run() { |
82 | final String path = data.getPath(); |
83 | Log.d(TAG, "opening: " + path); |
84 | final Map<File, Exception> errors = dictionaryService.open(new File(path)); |
85 | runOnUiThread(new Runnable() { |
86 | public void run() { |
87 | if (errors.size() == 0) { |
88 | Toast.makeText(getApplicationContext(), |
89 | getString(R.string.toastDictFileLoaded, path), |
90 | Toast.LENGTH_LONG).show(); |
91 | } |
92 | else { |
93 | Toast.makeText(getApplicationContext(), |
94 | getString(R.string.toastDictFileFailed, path), |
95 | Toast.LENGTH_LONG).show(); |
96 | } |
97 | finish(); |
98 | } |
99 | }); |
100 | } |
101 | }; |
102 | new Thread(r).start(); |
103 | Log.d(TAG, "started: " + data.getPath()); |
104 | } |
105 | } |
106 | |
107 | |
108 | if (action != null && action.equals(ACTION_NO_DICTIONARIES)) { |
109 | showNoDictionariesView(); |
110 | } else { |
111 | super.onDictionaryServiceConnected(); |
112 | } |
113 | } |
114 | |
115 | private void showNoDictionariesView() { |
116 | TextView messageView = (TextView) findViewById(R.id.dictionariesMessageView); |
117 | Button scanSDButton = (Button) findViewById(R.id.scanSDButton); |
118 | messageView.setVisibility(View.VISIBLE); |
119 | scanSDButton.setVisibility(View.VISIBLE); |
120 | listView.setVisibility(View.GONE); |
121 | } |
122 | |
123 | @Override |
124 | void onDictionaryServiceReady() { |
125 | if (aboutToFinish) { |
126 | return; |
127 | } |
128 | Log.d(TAG, "service ready"); |
129 | |
130 | if (dictionaryService.getDictionaries().isEmpty()) { |
131 | showNoDictionariesView(); |
132 | } else { |
133 | Intent intent = getIntent(); |
134 | String action = intent.getAction(); |
135 | Log.d(TAG, "Action: " + action); |
136 | if (action != null && action.equals(ACTION_NO_DICTIONARIES)) { |
137 | aboutToFinish = true; |
138 | Intent next = new Intent(); |
139 | next.setClass(this, LookupActivity.class); |
140 | Log.d(TAG, "Starting Lookup Activity"); |
141 | startActivity(next); |
142 | finish(); |
143 | } else { |
144 | TextView messageView = (TextView) findViewById(R.id.dictionariesMessageView); |
145 | Button scanSDButton = (Button) findViewById(R.id.scanSDButton); |
146 | messageView.setVisibility(View.GONE); |
147 | scanSDButton.setVisibility(View.GONE); |
148 | listView.setVisibility(View.VISIBLE); |
149 | dataAdapter = new DictListAdapter(dictionaryService |
150 | .getVolumes()); |
151 | listView.setAdapter(dataAdapter); |
152 | listView.setOnItemClickListener(dataAdapter); |
153 | listView.setOnItemLongClickListener(dataAdapter); |
154 | } |
155 | } |
156 | } |
157 | |
158 | @Override |
159 | void initUI() { |
160 | |
161 | setContentView(R.layout.dictionaries); |
162 | |
163 | listView = (ListView) findViewById(R.id.dictionariesList); |
164 | |
165 | Button scanSDButton = (Button) findViewById(R.id.scanSDButton); |
166 | scanSDButton.setOnClickListener(new View.OnClickListener() { |
167 | public void onClick(View v) { |
168 | scandSDCard(); |
169 | } |
170 | }); |
171 | |
172 | TextView messageView = (TextView) findViewById(R.id.dictionariesMessageView); |
173 | messageView.setMovementMethod(LinkMovementMethod.getInstance()); |
174 | messageView.setText(Html.fromHtml(getString(R.string.noDictionaries))); |
175 | |
176 | String appName = getString(R.string.appName); |
177 | setTitle(getString(R.string.titleDictionariesActivity, appName)); |
178 | try { |
179 | loadVerifyData(); |
180 | } catch (Exception e) { |
181 | Log.e(TAG, "Failed to load verify data", e); |
182 | } |
183 | } |
184 | |
185 | @Override |
186 | protected void onDestroy() { |
187 | super.onDestroy(); |
188 | if (dataAdapter != null) { |
189 | dataAdapter.destroy(); |
190 | } |
191 | } |
192 | |
193 | final class DictListAdapter extends BaseAdapter implements |
194 | AdapterView.OnItemClickListener, |
195 | AdapterView.OnItemLongClickListener |
196 | |
197 | { |
198 | LayoutInflater inflater; |
199 | List<List<Volume>> volumes; |
200 | Timer timer = new Timer(); |
201 | long TIME_UPDATE_PERIOD = 60 * 1000; |
202 | |
203 | @SuppressWarnings("unchecked") |
204 | public DictListAdapter(Map<UUID, List<Volume>> volumes) { |
205 | this.volumes = new ArrayList(); |
206 | this.volumes.addAll(volumes.values()); |
207 | inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
208 | timer.scheduleAtFixedRate(new TimerTask() { |
209 | public void run() { |
210 | updateView(); |
211 | } |
212 | }, TIME_UPDATE_PERIOD, TIME_UPDATE_PERIOD); |
213 | } |
214 | |
215 | public int getCount() { |
216 | return volumes.size(); |
217 | } |
218 | |
219 | public Object getItem(int position) { |
220 | return position; |
221 | } |
222 | |
223 | public long getItemId(int position) { |
224 | return position; |
225 | } |
226 | |
227 | public void destroy() { |
228 | timer.cancel(); |
229 | } |
230 | |
231 | public void onItemClick(AdapterView<?> parent, View view, int position, |
232 | long id) { |
233 | showDetail(position); |
234 | } |
235 | |
236 | private void showDetail(int position) { |
237 | Intent i = new Intent(DictionariesActivity.this, |
238 | DictionaryInfoActivity.class); |
239 | i.putExtra("volumeId", volumes.get(position).get(0).getId()); |
240 | startActivity(i); |
241 | } |
242 | |
243 | final class ProgressListener implements VerifyProgressListener { |
244 | |
245 | boolean proceed = true; |
246 | ProgressDialog progressDialog; |
247 | int max; |
248 | int verifiedCount = 0; |
249 | |
250 | ProgressListener(ProgressDialog progressDialog, int max) { |
251 | this.progressDialog = progressDialog; |
252 | this.max = max; |
253 | } |
254 | |
255 | public boolean updateProgress(final Volume d, final double progress) { |
256 | runOnUiThread(new Runnable() { |
257 | public void run() { |
258 | CharSequence m = getTitle(d, true); |
259 | progressDialog.setMessage(m); |
260 | progressDialog |
261 | .setProgress((int) (100 * progress / max)); |
262 | } |
263 | }); |
264 | return proceed; |
265 | } |
266 | |
267 | public void verified(final Volume d, final boolean ok) { |
268 | verifiedCount++; |
269 | Log.i(TAG, String.format("Verified %s: %s", |
270 | d.getDisplayTitle(), (ok ? "ok" : "corrupted"))); |
271 | if (!ok) { |
272 | recordVerifyData(d.getDictionaryId(), ok); |
273 | progressDialog.dismiss(); |
274 | CharSequence message = getString(R.string.msgDictCorruped, |
275 | getTitle(d, true)); |
276 | showError(message); |
277 | } else { |
278 | runOnUiThread(new Runnable() { |
279 | public void run() { |
280 | Toast.makeText( |
281 | DictionariesActivity.this, |
282 | getString(R.string.msgDictOk, getTitle(d, |
283 | true)), Toast.LENGTH_SHORT).show(); |
284 | } |
285 | }); |
286 | if (verifiedCount == max) { |
287 | recordVerifyData(d.getDictionaryId(), ok); |
288 | progressDialog.dismiss(); |
289 | } |
290 | } |
291 | } |
292 | } |
293 | |
294 | private void recordVerifyData(UUID uuid, boolean ok) { |
295 | VerifyRecord record = new VerifyRecord(); |
296 | record.uuid = uuid; |
297 | record.ok = ok; |
298 | record.date = new Date(); |
299 | verifyData.put(record.uuid, record); |
300 | try { |
301 | saveVerifyData(); |
302 | } catch (Exception e) { |
303 | Log.e(TAG, "Failed to save verify data", e); |
304 | } |
305 | updateView(); |
306 | } |
307 | |
308 | private void updateView() { |
309 | runOnUiThread(new Runnable() { |
310 | public void run() { |
311 | notifyDataSetChanged(); |
312 | } |
313 | }); |
314 | } |
315 | |
316 | private void showError(final CharSequence message) { |
317 | runOnUiThread(new Runnable() { |
318 | public void run() { |
319 | AlertDialog.Builder dialogBuilder = new AlertDialog.Builder( |
320 | DictionariesActivity.this); |
321 | dialogBuilder.setTitle(R.string.titleError).setMessage( |
322 | message).setNeutralButton(R.string.btnDismiss, |
323 | new OnClickListener() { |
324 | public void onClick(DialogInterface dialog, |
325 | int which) { |
326 | dialog.dismiss(); |
327 | } |
328 | }); |
329 | dialogBuilder.show(); |
330 | } |
331 | }); |
332 | } |
333 | |
334 | public boolean onItemLongClick(AdapterView<?> parent, View view, |
335 | int position, long id) { |
336 | verify(position); |
337 | return true; |
338 | } |
339 | |
340 | private void verify(int position) { |
341 | final List<Volume> allDictVols = volumes.get(position); |
342 | final ProgressDialog progressDialog = new ProgressDialog( |
343 | DictionariesActivity.this); |
344 | progressDialog.setIndeterminate(false); |
345 | progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); |
346 | progressDialog.setTitle(R.string.titleVerifying); |
347 | progressDialog.setMessage(getTitle(allDictVols.get(0), false)); |
348 | progressDialog.setCancelable(true); |
349 | final ProgressListener progressListener = new ProgressListener( |
350 | progressDialog, allDictVols.size()); |
351 | |
352 | Runnable verify = new Runnable() { |
353 | public void run() { |
354 | for (Volume d : allDictVols) { |
355 | try { |
356 | d.verify(progressListener); |
357 | } catch (Exception e) { |
358 | Log.e(TAG, "There was an error verifying volume " |
359 | + d.getId(), e); |
360 | progressListener.proceed = false; |
361 | progressDialog.dismiss(); |
362 | showError(getString(R.string.msgErrorVerifying, d |
363 | .getDisplayTitle(), e.getLocalizedMessage())); |
364 | } |
365 | } |
366 | } |
367 | }; |
368 | |
369 | progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, |
370 | getString(R.string.btnCancel), new OnClickListener() { |
371 | public void onClick(DialogInterface dialog, int which) { |
372 | progressListener.proceed = false; |
373 | } |
374 | }); |
375 | progressDialog.setOnCancelListener(new OnCancelListener() { |
376 | public void onCancel(DialogInterface dialog) { |
377 | progressListener.proceed = false; |
378 | } |
379 | }); |
380 | Thread t = new Thread(verify); |
381 | t.setPriority(Thread.MIN_PRIORITY); |
382 | t.start(); |
383 | progressDialog.show(); |
384 | } |
385 | |
386 | CharSequence getTitle(Volume d, boolean withVol) { |
387 | StringBuilder s = new StringBuilder(d.getDisplayTitle(withVol)); |
388 | if (d.metadata.version != null) { |
389 | s.append(" ").append(d.metadata.version); |
390 | } |
391 | return s; |
392 | } |
393 | |
394 | public View getView(int position, View convertView, ViewGroup parent) { |
395 | List<Volume> allDictVols = volumes.get(position); |
396 | int volCount = allDictVols.size(); |
397 | Volume d = allDictVols.get(0); |
398 | |
399 | TwoLineListItem view = (convertView != null) ? (TwoLineListItem) convertView |
400 | : createView(parent); |
401 | |
402 | view.getText1().setText(getTitle(d, false)); |
403 | |
404 | Resources r = getResources(); |
405 | String articleStr = r.getQuantityString(R.plurals.articles, |
406 | d.metadata.article_count, d.metadata.article_count); |
407 | String totalVolumesStr = r.getQuantityString(R.plurals.volumes, |
408 | d.header.of, d.header.of); |
409 | String volumesStr = r.getQuantityString(R.plurals.volumes, |
410 | volCount, volCount); |
411 | String shortInfo = r.getString(R.string.shortDictInfo, articleStr, |
412 | totalVolumesStr, volumesStr); |
413 | if (verifyData.containsKey(d.getDictionaryId())) { |
414 | VerifyRecord record = verifyData.get(d.getDictionaryId()); |
415 | CharSequence dateStr = DateUtils |
416 | .getRelativeTimeSpanString(record.date.getTime()); |
417 | String resultStr = getString(record.ok ? R.string.verifyOk |
418 | : R.string.verifyCorrupted); |
419 | view.getText2().setText( |
420 | getString(R.string.msgDataIntegrityVerified, shortInfo, |
421 | dateStr, resultStr)); |
422 | } else { |
423 | view.getText2().setText( |
424 | getString(R.string.msgDataIntegrityNotVerified, |
425 | shortInfo)); |
426 | } |
427 | return view; |
428 | } |
429 | |
430 | private TwoLineListItem createView(ViewGroup parent) { |
431 | TwoLineListItem item = (TwoLineListItem) inflater.inflate( |
432 | android.R.layout.simple_list_item_2, parent, false); |
433 | return item; |
434 | } |
435 | |
436 | } |
437 | |
438 | final static int MENU_INFO = 1; |
439 | final static int MENU_VERIFY = 2; |
440 | final static int MENU_REFRESH = 3; |
441 | |
442 | @Override |
443 | public boolean onCreateOptionsMenu(Menu menu) { |
444 | menu.add(0, MENU_INFO, 0, R.string.mnDictDetails).setIcon( |
445 | android.R.drawable.ic_menu_info_details); |
446 | menu.add(0, MENU_VERIFY, 1, R.string.mnDictVerify).setIcon( |
447 | android.R.drawable.ic_menu_manage); |
448 | menu.add(0, MENU_REFRESH, 2, R.string.mnDictRefresh).setIcon( |
449 | R.drawable.ic_menu_refresh); |
450 | return true; |
451 | } |
452 | |
453 | @Override |
454 | public boolean onPrepareOptionsMenu(Menu menu) { |
455 | int selected = listView.getSelectedItemPosition(); |
456 | boolean validSelection = selected != ListView.INVALID_POSITION; |
457 | menu.getItem(0).setEnabled(validSelection); |
458 | menu.getItem(1).setEnabled(validSelection); |
459 | menu.getItem(2).setEnabled(true); |
460 | return true; |
461 | } |
462 | |
463 | @Override |
464 | public boolean onOptionsItemSelected(MenuItem item) { |
465 | int selected = listView.getSelectedItemPosition(); |
466 | boolean validSelection = selected != ListView.INVALID_POSITION; |
467 | switch (item.getItemId()) { |
468 | case MENU_INFO: |
469 | if (validSelection) { |
470 | dataAdapter.showDetail(selected); |
471 | } |
472 | break; |
473 | case MENU_VERIFY: |
474 | if (validSelection) { |
475 | dataAdapter.verify(selected); |
476 | } |
477 | break; |
478 | case MENU_REFRESH: |
479 | scandSDCard(); |
480 | break; |
481 | } |
482 | return true; |
483 | } |
484 | |
485 | private void scandSDCard() { |
486 | new Thread(new Runnable() { |
487 | public void run() { |
488 | dictionaryService.refresh(); |
489 | runOnUiThread(new Runnable() { |
490 | public void run() { |
491 | onDictionaryServiceReady(); |
492 | } |
493 | }); |
494 | } |
495 | }).start(); |
496 | } |
497 | |
498 | void saveVerifyData() throws IOException { |
499 | File verifyDir = getDir("verify", 0); |
500 | File verifyFile = new File(verifyDir, "verifydata"); |
501 | FileOutputStream fout = new FileOutputStream(verifyFile); |
502 | ObjectOutputStream oout = new ObjectOutputStream(fout); |
503 | oout.writeObject(verifyData); |
504 | } |
505 | |
506 | @SuppressWarnings("unchecked") |
507 | void loadVerifyData() throws IOException, ClassNotFoundException { |
508 | File verifyDir = getDir("verify", 0); |
509 | File verifyFile = new File(verifyDir, "verifydata"); |
510 | if (verifyFile.exists()) { |
511 | FileInputStream fin = new FileInputStream(verifyFile); |
512 | ObjectInputStream oin = new ObjectInputStream(fin); |
513 | verifyData = (Map<UUID, VerifyRecord>) oin.readObject(); |
514 | } |
515 | } |
516 | } |