| 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 | } |