]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryManagerActivity.java
be02da3c329cbefc10b215055bca65378e590ab6
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryManagerActivity.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.Manifest;
18 import android.app.AlertDialog;
19 import android.app.DownloadManager;
20 import android.app.DownloadManager.Request;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.Editor;
27 import android.content.pm.PackageManager;
28 import android.database.Cursor;
29 import android.net.Uri;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.os.Environment;
33 import android.os.Handler;
34 import android.preference.PreferenceManager;
35 import android.provider.Settings;
36 import android.support.annotation.NonNull;
37 import android.support.v4.app.ActivityCompat;
38 import android.support.v4.content.ContextCompat;
39 import android.support.v4.view.MenuItemCompat;
40 import android.support.v7.app.ActionBar;
41 import android.support.v7.app.AppCompatActivity;
42 import android.support.v7.widget.SearchView;
43 import android.support.v7.widget.SearchView.OnQueryTextListener;
44 import android.support.v7.widget.Toolbar;
45 import android.text.InputType;
46 import android.util.Log;
47 import android.view.ContextMenu;
48 import android.view.ContextMenu.ContextMenuInfo;
49 import android.view.LayoutInflater;
50 import android.view.Menu;
51 import android.view.MenuItem;
52 import android.view.View;
53 import android.view.View.OnClickListener;
54 import android.view.ViewGroup;
55 import android.view.inputmethod.EditorInfo;
56 import android.widget.AdapterView.AdapterContextMenuInfo;
57 import android.widget.BaseAdapter;
58 import android.widget.Button;
59 import android.widget.CompoundButton;
60 import android.widget.CompoundButton.OnCheckedChangeListener;
61 import android.widget.FrameLayout;
62 import android.widget.ImageButton;
63 import android.widget.LinearLayout;
64 import android.widget.ListAdapter;
65 import android.widget.ListView;
66 import android.widget.TextView;
67 import android.widget.Toast;
68 import android.widget.ToggleButton;
69
70 import com.hughes.android.dictionary.DictionaryInfo.IndexInfo;
71 import com.hughes.android.util.IntentLauncher;
72
73 import java.io.BufferedInputStream;
74 import java.io.File;
75 import java.io.FileInputStream;
76 import java.io.FileOutputStream;
77 import java.io.IOException;
78 import java.io.InputStream;
79 import java.net.MalformedURLException;
80 import java.net.URL;
81 import java.nio.ByteBuffer;
82 import java.nio.channels.FileChannel;
83 import java.util.Collections;
84 import java.util.HashSet;
85 import java.util.List;
86 import java.util.Set;
87 import java.util.regex.Pattern;
88 import java.util.zip.ZipEntry;
89 import java.util.zip.ZipInputStream;
90
91 // Right-click:
92 //  Delete, move to top.
93
94 public class DictionaryManagerActivity extends AppCompatActivity {
95
96     private static final String LOG = "QuickDic";
97     private static boolean blockAutoLaunch = false;
98
99     private ListView listView;
100     private ListView getListView() {
101         if (listView == null) {
102             listView = (ListView)findViewById(android.R.id.list);
103         }
104         return listView;
105     }
106     private void setListAdapter(ListAdapter adapter) {
107         getListView().setAdapter(adapter);
108     }
109     private ListAdapter getListAdapter() {
110         return getListView().getAdapter();
111     }
112
113     // For DownloadManager bug workaround
114     private final Set<Long> finishedDownloadIds = new HashSet<>();
115
116     private DictionaryApplication application;
117
118     private SearchView filterSearchView;
119     private ToggleButton showDownloadable;
120
121     private LinearLayout dictionariesOnDeviceHeaderRow;
122     private LinearLayout downloadableDictionariesHeaderRow;
123
124     private Handler uiHandler;
125
126     private final Runnable dictionaryUpdater = new Runnable() {
127         @Override
128         public void run() {
129             if (uiHandler == null) {
130                 return;
131             }
132             uiHandler.post(new Runnable() {
133                 @Override
134                 public void run() {
135                     setMyListAdapter();
136                 }
137             });
138         }
139     };
140
141     private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
142         @Override
143         public synchronized void onReceive(Context context, Intent intent) {
144             final String action = intent.getAction();
145
146             if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) {
147                 startActivity(DictionaryManagerActivity.getLaunchIntent(getApplicationContext()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP));
148             }
149             if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
150                 final long downloadId = intent.getLongExtra(
151                                             DownloadManager.EXTRA_DOWNLOAD_ID, 0);
152                 if (finishedDownloadIds.contains(downloadId)) return; // ignore double notifications
153                 final DownloadManager.Query query = new DownloadManager.Query();
154                 query.setFilterById(downloadId);
155                 final DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
156                 final Cursor cursor = downloadManager.query(query);
157
158                 if (cursor == null || !cursor.moveToFirst()) {
159                     Log.e(LOG, "Couldn't find download.");
160                     return;
161                 }
162
163                 final String dest = cursor
164                                     .getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
165                 final int status = cursor
166                                    .getInt(cursor
167                                            .getColumnIndex(DownloadManager.COLUMN_STATUS));
168                 if (DownloadManager.STATUS_SUCCESSFUL != status) {
169                     final int reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON));
170                     Log.w(LOG,
171                           "Download failed: status=" + status +
172                           ", reason=" + reason);
173                     String msg = Integer.toString(reason);
174                     switch (reason) {
175                     case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
176                         msg = "File exists";
177                         break;
178                     case DownloadManager.ERROR_FILE_ERROR:
179                         msg = "File error";
180                         break;
181                     case DownloadManager.ERROR_INSUFFICIENT_SPACE:
182                         msg = "Not enough space";
183                         break;
184                     }
185                     new AlertDialog.Builder(context).setTitle(getString(R.string.error)).setMessage(getString(R.string.downloadFailed, msg)).setNeutralButton("Close", null).show();
186                     return;
187                 }
188
189                 Log.w(LOG, "Download finished: " + dest + " Id: " + downloadId);
190                 if (!isFinishing())
191                     Toast.makeText(context, getString(R.string.unzippingDictionary, dest),
192                                    Toast.LENGTH_LONG).show();
193
194                 if (unzipInstall(context, Uri.parse(dest), dest, true)) {
195                     finishedDownloadIds.add(downloadId);
196                     Log.w(LOG, "Unzipping finished: " + dest + " Id: " + downloadId);
197                 }
198             }
199         }
200     };
201
202     private boolean unzipInstall(Context context, Uri zipUri, String dest, boolean delete) {
203         File localZipFile = null;
204         InputStream zipFileStream = null;
205         ZipInputStream zipFile = null;
206         FileOutputStream zipOut = null;
207         boolean result = false;
208         try {
209             if (zipUri.getScheme().equals("content")) {
210                 zipFileStream = context.getContentResolver().openInputStream(zipUri);
211                 localZipFile = null;
212             } else {
213                 localZipFile = new File(zipUri.getPath());
214                 try {
215                     zipFileStream = new FileInputStream(localZipFile);
216                 } catch (Exception e) {
217                     if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
218                         ActivityCompat.requestPermissions(this,
219                                                   new String[] {Manifest.permission.READ_EXTERNAL_STORAGE,
220                                                           Manifest.permission.WRITE_EXTERNAL_STORAGE
221                                                                }, 0);
222                         return false;
223                     }
224                     throw e;
225                 }
226             }
227             zipFile = new ZipInputStream(new BufferedInputStream(zipFileStream));
228             ZipEntry zipEntry;
229             while ((zipEntry = zipFile.getNextEntry()) != null) {
230                 // Note: this check prevents security issues like accidental path
231                 // traversal, which unfortunately ZipInputStream has no protection against.
232                 // So take extra care when changing it.
233                 if (!Pattern.matches("[-A-Za-z]+\\.quickdic", zipEntry.getName())) {
234                     Log.w(LOG, "Invalid zip entry: " + zipEntry.getName());
235                     continue;
236                 }
237                 Log.d(LOG, "Unzipping entry: " + zipEntry.getName());
238                 File targetFile = new File(application.getDictDir(), zipEntry.getName());
239                 if (targetFile.exists()) {
240                     targetFile.renameTo(new File(targetFile.getAbsolutePath().replace(".quickdic", ".bak.quickdic")));
241                     targetFile = new File(application.getDictDir(), zipEntry.getName());
242                 }
243                 zipOut = new FileOutputStream(targetFile);
244                 copyStream(zipFile, zipOut);
245             }
246             application.backgroundUpdateDictionaries(dictionaryUpdater);
247             if (!isFinishing())
248                 Toast.makeText(context, getString(R.string.installationFinished, dest),
249                                Toast.LENGTH_LONG).show();
250             result = true;
251         } catch (Exception e) {
252             String msg = getString(R.string.unzippingFailed, dest + ": " + e.getMessage());
253             File dir = application.getDictDir();
254             if (!dir.canWrite() || !DictionaryApplication.checkFileCreate(dir)) {
255                 msg = getString(R.string.notWritable, dir.getAbsolutePath());
256             }
257             new AlertDialog.Builder(context).setTitle(getString(R.string.error)).setMessage(msg).setNeutralButton("Close", null).show();
258             Log.e(LOG, "Failed to unzip.", e);
259         } finally {
260             try {
261                 if (zipOut != null) zipOut.close();
262             } catch (IOException ignored) {}
263             try {
264                 if (zipFile != null) zipFile.close();
265             } catch (IOException ignored) {}
266             try {
267                 if (zipFileStream != null) zipFileStream.close();
268             } catch (IOException ignored) {}
269             if (localZipFile != null && delete) //noinspection ResultOfMethodCallIgnored
270                 localZipFile.delete();
271         }
272         return result;
273     }
274
275     public static Intent getLaunchIntent(Context c) {
276         final Intent intent = new Intent(c, DictionaryManagerActivity.class);
277         intent.putExtra(C.CAN_AUTO_LAUNCH_DICT, false);
278         return intent;
279     }
280
281     private void readableCheckAndError(boolean requestPermission) {
282         final File dictDir = application.getDictDir();
283         if (dictDir.canRead() && dictDir.canExecute()) return;
284         blockAutoLaunch = true;
285         if (requestPermission &&
286                 ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
287             ActivityCompat.requestPermissions(this,
288                                               new String[] {Manifest.permission.READ_EXTERNAL_STORAGE,
289                                                       Manifest.permission.WRITE_EXTERNAL_STORAGE
290                                                            }, 0);
291             return;
292         }
293         blockAutoLaunch = true;
294
295         AlertDialog.Builder builder = new AlertDialog.Builder(getListView().getContext());
296         builder.setTitle(getString(R.string.error));
297         builder.setMessage(getString(
298                                R.string.unableToReadDictionaryDir,
299                                dictDir.getAbsolutePath(),
300                                Environment.getExternalStorageDirectory()));
301         builder.setNeutralButton("Close", null);
302         builder.create().show();
303     }
304
305     @Override
306     public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
307         readableCheckAndError(false);
308
309         application.backgroundUpdateDictionaries(dictionaryUpdater);
310
311         setMyListAdapter();
312     }
313
314     @Override
315     public void onCreate(Bundle savedInstanceState) {
316         DictionaryApplication.INSTANCE.init(getApplicationContext());
317         application = DictionaryApplication.INSTANCE;
318         // This must be first, otherwise the action bar doesn't get
319         // styled properly.
320         setTheme(application.getSelectedTheme().themeId);
321
322         super.onCreate(savedInstanceState);
323         Log.d(LOG, "onCreate:" + this);
324
325         setTheme(application.getSelectedTheme().themeId);
326
327         blockAutoLaunch = false;
328
329         // UI init.
330         setContentView(R.layout.dictionary_manager_activity);
331
332         dictionariesOnDeviceHeaderRow = (LinearLayout) LayoutInflater.from(
333                                             getListView().getContext()).inflate(
334                                             R.layout.dictionary_manager_header_row_on_device, getListView(), false);
335
336         downloadableDictionariesHeaderRow = (LinearLayout) LayoutInflater.from(
337                                                 getListView().getContext()).inflate(
338                                                 R.layout.dictionary_manager_header_row_downloadable, getListView(), false);
339
340         showDownloadable = downloadableDictionariesHeaderRow
341                            .findViewById(R.id.hideDownloadable);
342         showDownloadable.setOnCheckedChangeListener(new OnCheckedChangeListener() {
343             @Override
344             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
345                 onShowDownloadableChanged();
346             }
347         });
348
349         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
350         final String thanksForUpdatingLatestVersion = getString(R.string.thanksForUpdatingVersion);
351         if (!prefs.getString(C.THANKS_FOR_UPDATING_VERSION, "").equals(
352                     thanksForUpdatingLatestVersion)) {
353             blockAutoLaunch = true;
354             startActivity(HtmlDisplayActivity.getWhatsNewLaunchIntent(getApplicationContext()));
355             prefs.edit().putString(C.THANKS_FOR_UPDATING_VERSION, thanksForUpdatingLatestVersion)
356             .commit();
357         }
358         IntentFilter downloadManagerIntents = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
359         downloadManagerIntents.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED);
360         registerReceiver(broadcastReceiver, downloadManagerIntents);
361
362         setMyListAdapter();
363         registerForContextMenu(getListView());
364         getListView().setItemsCanFocus(true);
365
366         readableCheckAndError(true);
367
368         onCreateSetupActionBar();
369
370         final Intent intent = getIntent();
371         if (intent != null && intent.getAction() != null &&
372             intent.getAction().equals(Intent.ACTION_VIEW)) {
373             blockAutoLaunch = true;
374             Uri uri = intent.getData();
375             unzipInstall(this, uri, uri.getLastPathSegment(), false);
376         }
377     }
378
379     private void onCreateSetupActionBar() {
380         ActionBar actionBar = getSupportActionBar();
381         actionBar.setDisplayShowTitleEnabled(false);
382         actionBar.setDisplayShowHomeEnabled(false);
383         actionBar.setDisplayHomeAsUpEnabled(false);
384
385         filterSearchView = new SearchView(getSupportActionBar().getThemedContext());
386         filterSearchView.setIconifiedByDefault(false);
387         // filterSearchView.setIconified(false); // puts the magnifying glass in
388         // the
389         // wrong place.
390         filterSearchView.setQueryHint(getString(R.string.searchText));
391         filterSearchView.setSubmitButtonEnabled(false);
392         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
393                 FrameLayout.LayoutParams.WRAP_CONTENT);
394         filterSearchView.setLayoutParams(lp);
395         filterSearchView.setInputType(InputType.TYPE_CLASS_TEXT);
396         filterSearchView.setImeOptions(
397             EditorInfo.IME_ACTION_DONE |
398             EditorInfo.IME_FLAG_NO_EXTRACT_UI |
399             // EditorInfo.IME_FLAG_NO_FULLSCREEN | // Requires API
400             // 11
401             EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
402
403         filterSearchView.setOnQueryTextListener(new OnQueryTextListener() {
404             @Override
405             public boolean onQueryTextSubmit(String query) {
406                 filterSearchView.clearFocus();
407                 return false;
408             }
409
410             @Override
411             public boolean onQueryTextChange(String filterText) {
412                 setMyListAdapter();
413                 return true;
414             }
415         });
416         filterSearchView.setFocusable(true);
417
418         actionBar.setCustomView(filterSearchView);
419         actionBar.setDisplayShowCustomEnabled(true);
420
421         // Avoid wasting space on large left inset
422         Toolbar tb = (Toolbar)filterSearchView.getParent();
423         tb.setContentInsetsRelative(0, 0);
424     }
425
426     @Override
427     public void onDestroy() {
428         super.onDestroy();
429         unregisterReceiver(broadcastReceiver);
430     }
431
432     private static void copyStream(final InputStream ins, final FileOutputStream outs)
433     throws IOException {
434         ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 64);
435         FileChannel out = outs.getChannel();
436         int bytesRead;
437         int pos = 0;
438         final byte[] bytes = new byte[1024 * 64];
439         do {
440             bytesRead = ins.read(bytes, pos, bytes.length - pos);
441             if (bytesRead != -1) pos += bytesRead;
442             if (bytesRead == -1 ? pos != 0 : 2*pos >= bytes.length) {
443                 buf.put(bytes, 0, pos);
444                 pos = 0;
445                 buf.flip();
446                 while (buf.hasRemaining()) out.write(buf);
447                 buf.clear();
448             }
449         } while (bytesRead != -1);
450     }
451
452     @Override
453     protected void onStart() {
454         super.onStart();
455         uiHandler = new Handler();
456     }
457
458     @Override
459     protected void onStop() {
460         super.onStop();
461         uiHandler = null;
462     }
463
464     @Override
465     protected void onResume() {
466         super.onResume();
467
468         if (PreferenceActivity.prefsMightHaveChanged) {
469             PreferenceActivity.prefsMightHaveChanged = false;
470             finish();
471             startActivity(getIntent());
472         }
473
474         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
475         showDownloadable.setChecked(prefs.getBoolean(C.SHOW_DOWNLOADABLE, true));
476
477         if (!blockAutoLaunch &&
478                 getIntent().getBooleanExtra(C.CAN_AUTO_LAUNCH_DICT, true) &&
479                 prefs.contains(C.DICT_FILE) &&
480                 prefs.contains(C.INDEX_SHORT_NAME)) {
481             Log.d(LOG, "Skipping DictionaryManager, going straight to dictionary.");
482             startActivity(DictionaryActivity.getLaunchIntent(getApplicationContext(),
483                           new File(prefs.getString(C.DICT_FILE, "")),
484                           prefs.getString(C.INDEX_SHORT_NAME, ""),
485                           ""));
486             finish();
487             return;
488         }
489
490         // Remove the active dictionary from the prefs so we won't autolaunch
491         // next time.
492         prefs.edit().remove(C.DICT_FILE).remove(C.INDEX_SHORT_NAME).commit();
493
494         application.backgroundUpdateDictionaries(dictionaryUpdater);
495
496         setMyListAdapter();
497     }
498
499     @Override
500     public boolean onCreateOptionsMenu(final Menu menu) {
501         if ("true".equals(Settings.System.getString(getContentResolver(), "firebase.test.lab")))
502         {
503             return false; // testing the menu is not very interesting
504         }
505         final MenuItem sort = menu.add(getString(R.string.sortDicts));
506         MenuItemCompat.setShowAsAction(sort, MenuItem.SHOW_AS_ACTION_NEVER);
507         sort.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
508             public boolean onMenuItemClick(final MenuItem menuItem) {
509                 application.sortDictionaries();
510                 setMyListAdapter();
511                 return true;
512             }
513         });
514
515         final MenuItem browserDownload = menu.add(getString(R.string.browserDownload));
516         MenuItemCompat.setShowAsAction(browserDownload, MenuItem.SHOW_AS_ACTION_NEVER);
517         browserDownload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
518             public boolean onMenuItemClick(final MenuItem menuItem) {
519                 final Intent intent = new Intent(Intent.ACTION_VIEW);
520                 intent.setData(Uri
521                                .parse("https://github.com/rdoeffinger/Dictionary/releases/v0.2-dictionaries"));
522                 startActivity(intent);
523                 return false;
524             }
525         });
526
527         DictionaryApplication.onCreateGlobalOptionsMenu(this, menu);
528         return true;
529     }
530
531     @Override
532     public void onCreateContextMenu(final ContextMenu menu, final View view,
533                                     final ContextMenuInfo menuInfo) {
534         super.onCreateContextMenu(menu, view, menuInfo);
535         Log.d(LOG, "onCreateContextMenu, " + menuInfo);
536
537         final AdapterContextMenuInfo adapterContextMenuInfo =
538             (AdapterContextMenuInfo) menuInfo;
539         final int position = adapterContextMenuInfo.position;
540         final MyListAdapter.Row row = (MyListAdapter.Row) getListAdapter().getItem(position);
541
542         if (row.dictionaryInfo == null) {
543             return;
544         }
545
546         if (position > 0 && row.onDevice) {
547             final android.view.MenuItem moveToTopMenuItem =
548                 menu.add(R.string.moveToTop);
549             moveToTopMenuItem.setOnMenuItemClickListener(new
550             android.view.MenuItem.OnMenuItemClickListener() {
551                 @Override
552                 public boolean onMenuItemClick(android.view.MenuItem item) {
553                     application.moveDictionaryToTop(row.dictionaryInfo);
554                     setMyListAdapter();
555                     return true;
556                 }
557             });
558         }
559
560         if (row.onDevice) {
561             final android.view.MenuItem deleteMenuItem = menu.add(R.string.deleteDictionary);
562             deleteMenuItem
563             .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
564                 @Override
565                 public boolean onMenuItemClick(android.view.MenuItem item) {
566                     application.deleteDictionary(row.dictionaryInfo);
567                     setMyListAdapter();
568                     return true;
569                 }
570             });
571         }
572     }
573
574     private void onShowDownloadableChanged() {
575         setMyListAdapter();
576         Editor prefs = PreferenceManager.getDefaultSharedPreferences(this).edit();
577         prefs.putBoolean(C.SHOW_DOWNLOADABLE, showDownloadable.isChecked());
578         prefs.commit();
579     }
580
581     class MyListAdapter extends BaseAdapter {
582
583         final List<DictionaryInfo> dictionariesOnDevice;
584         final List<DictionaryInfo> downloadableDictionaries;
585
586         class Row {
587             final DictionaryInfo dictionaryInfo;
588             final boolean onDevice;
589
590             private Row(DictionaryInfo dictionaryInfo, boolean onDevice) {
591                 this.dictionaryInfo = dictionaryInfo;
592                 this.onDevice = onDevice;
593             }
594         }
595
596         private MyListAdapter(final String[] filters) {
597             dictionariesOnDevice = application.getDictionariesOnDevice(filters);
598             if (showDownloadable.isChecked()) {
599                 downloadableDictionaries = application.getDownloadableDictionaries(filters);
600             } else {
601                 downloadableDictionaries = Collections.emptyList();
602             }
603         }
604
605         @Override
606         public int getCount() {
607             return 2 + dictionariesOnDevice.size() + downloadableDictionaries.size();
608         }
609
610         @Override
611         public Row getItem(int position) {
612             if (position == 0) {
613                 return new Row(null, true);
614             }
615             position -= 1;
616
617             if (position < dictionariesOnDevice.size()) {
618                 return new Row(dictionariesOnDevice.get(position), true);
619             }
620             position -= dictionariesOnDevice.size();
621
622             if (position == 0) {
623                 return new Row(null, false);
624             }
625             position -= 1;
626
627             assert position < downloadableDictionaries.size();
628             return new Row(downloadableDictionaries.get(position), false);
629         }
630
631         @Override
632         public long getItemId(int position) {
633             return position;
634         }
635
636         @Override
637         public int getViewTypeCount() {
638             return 3;
639         }
640
641         @Override
642         public int getItemViewType(int position) {
643             final Row row = getItem(position);
644             if (row.dictionaryInfo == null) {
645                 return row.onDevice ? 0 : 1;
646             }
647             assert row.dictionaryInfo.indexInfos.size() <= 2;
648             return 2;
649         }
650
651         @Override
652         public View getView(int position, View convertView, ViewGroup parent) {
653             if (convertView == dictionariesOnDeviceHeaderRow ||
654                 convertView == downloadableDictionariesHeaderRow) {
655                 return convertView;
656             }
657
658             final Row row = getItem(position);
659
660             if (row.dictionaryInfo == null) {
661                 assert convertView == null;
662                 return row.onDevice ? dictionariesOnDeviceHeaderRow : downloadableDictionariesHeaderRow;
663             }
664             return createDictionaryRow(row.dictionaryInfo, parent, convertView, row.onDevice);
665         }
666
667     }
668
669     private void setMyListAdapter() {
670         final String filter = filterSearchView == null ? "" : filterSearchView.getQuery()
671                               .toString();
672         final String[] filters = filter.trim().toLowerCase().split("(\\s|-)+");
673         setListAdapter(new MyListAdapter(filters));
674     }
675
676     private boolean isDownloadActive(String downloadUrl, boolean cancel) {
677         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
678         final DownloadManager.Query query = new DownloadManager.Query();
679         query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING);
680         final Cursor cursor = downloadManager.query(query);
681
682         // Due to a bug, cursor is null instead of empty when
683         // the download manager is disabled.
684         if (cursor == null) {
685             if (cancel) {
686                 String msg = getString(R.string.downloadManagerQueryFailed);
687                 new AlertDialog.Builder(DictionaryManagerActivity.this).setTitle(getString(R.string.error))
688                 .setMessage(getString(R.string.downloadFailed, msg))
689                 .setNeutralButton("Close", null).show();
690             }
691             return cancel;
692         }
693
694         String destFile;
695         try {
696             destFile = new File(new URL(downloadUrl).getPath()).getName();
697         } catch (MalformedURLException e) {
698             throw new RuntimeException("Invalid download URL!", e);
699         }
700         while (cursor.moveToNext()) {
701             if (downloadUrl.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI))))
702                 break;
703             if (destFile.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))))
704                 break;
705         }
706         boolean active = !cursor.isAfterLast();
707         if (active && cancel) {
708             long downloadId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
709             finishedDownloadIds.add(downloadId);
710             downloadManager.remove(downloadId);
711         }
712         cursor.close();
713         return active;
714     }
715
716     private View createDictionaryRow(final DictionaryInfo dictionaryInfo,
717                                      final ViewGroup parent, View row, boolean canLaunch) {
718
719         if (row == null) {
720             row = LayoutInflater.from(parent.getContext()).inflate(
721                        R.layout.dictionary_manager_row, parent, false);
722         }
723         final TextView name = row.findViewById(R.id.dictionaryName);
724         final TextView details = row.findViewById(R.id.dictionaryDetails);
725         name.setText(application.getDictionaryName(dictionaryInfo.uncompressedFilename));
726
727         final boolean updateAvailable = application.updateAvailable(dictionaryInfo);
728         final Button downloadButton = row.findViewById(R.id.downloadButton);
729         final DictionaryInfo downloadable = application.getDownloadable(dictionaryInfo.uncompressedFilename);
730         boolean broken = false;
731         if (!dictionaryInfo.isValid()) {
732             broken = true;
733             canLaunch = false;
734         }
735         if (downloadable != null && (!canLaunch || updateAvailable)) {
736             downloadButton
737             .setText(getString(
738                          R.string.downloadButton,
739                          downloadable.zipBytes / 1024.0 / 1024.0));
740             downloadButton.setMinWidth(application.languageButtonPixels * 3 / 2);
741             downloadButton.setOnClickListener(new OnClickListener() {
742                 @Override
743                 public void onClick(View arg0) {
744                     downloadDictionary(downloadable.downloadUrl, downloadable.zipBytes, downloadButton);
745                 }
746             });
747             downloadButton.setVisibility(View.VISIBLE);
748
749             if (isDownloadActive(downloadable.downloadUrl, false))
750                 downloadButton.setText("X");
751         } else {
752             downloadButton.setVisibility(View.GONE);
753         }
754
755         LinearLayout buttons = row.findViewById(R.id.dictionaryLauncherButtons);
756
757         final List<IndexInfo> sortedIndexInfos = application
758                 .sortedIndexInfos(dictionaryInfo.indexInfos);
759         final StringBuilder builder = new StringBuilder();
760         if (updateAvailable) {
761             builder.append(getString(R.string.updateAvailable));
762         }
763         assert buttons.getChildCount() == 4;
764         for (int i = 0; i < 2; i++) {
765             final Button textButton = (Button)buttons.getChildAt(2*i);
766             final ImageButton imageButton = (ImageButton)buttons.getChildAt(2*i + 1);
767             if (i >= sortedIndexInfos.size()) {
768                 textButton.setVisibility(View.GONE);
769                 imageButton.setVisibility(View.GONE);
770                 continue;
771             }
772             final IndexInfo indexInfo = sortedIndexInfos.get(i);
773             final View button = IsoUtils.INSTANCE.setupButton(textButton, imageButton,
774                     indexInfo);
775
776             if (canLaunch) {
777                 button.setOnClickListener(
778                     new IntentLauncher(buttons.getContext(),
779                                        DictionaryActivity.getLaunchIntent(getApplicationContext(),
780                                                application.getPath(dictionaryInfo.uncompressedFilename),
781                                                indexInfo.shortName, "")));
782
783             }
784             button.setEnabled(canLaunch);
785             button.setFocusable(canLaunch);
786             if (builder.length() != 0) {
787                 builder.append("; ");
788             }
789             builder.append(getString(R.string.indexInfo, indexInfo.shortName,
790                                      indexInfo.mainTokenCount));
791         }
792         builder.append("; ");
793         builder.append(getString(R.string.downloadButton, dictionaryInfo.uncompressedBytes / 1024.0 / 1024.0));
794         if (broken) {
795             name.setText("Broken: " + application.getDictionaryName(dictionaryInfo.uncompressedFilename));
796             builder.append("; Cannot be used, redownload, check hardware/file system");
797         }
798         details.setText(builder.toString());
799
800         if (canLaunch) {
801             row.setOnClickListener(new IntentLauncher(parent.getContext(),
802                                    DictionaryActivity.getLaunchIntent(getApplicationContext(),
803                                            application.getPath(dictionaryInfo.uncompressedFilename),
804                                            dictionaryInfo.indexInfos.get(0).shortName, "")));
805             // do not setFocusable, for keyboard navigation
806             // offering only the index buttons is better.
807         }
808         row.setClickable(canLaunch);
809         // Allow deleting, even if we cannot open
810         row.setLongClickable(broken || canLaunch);
811         row.setBackgroundResource(android.R.drawable.menuitem_background);
812
813         return row;
814     }
815
816     private synchronized void downloadDictionary(final String downloadUrl, long bytes, Button downloadButton) {
817         if (isDownloadActive(downloadUrl, true)) {
818             downloadButton
819             .setText(getString(
820                          R.string.downloadButton,
821                          bytes / 1024.0 / 1024.0));
822             return;
823         }
824         // API 19 and earlier have issues with github URLs, both http and https.
825         // Really old (~API 10) DownloadManager cannot handle https at all.
826         // Work around both with in one.
827         String altUrl = downloadUrl.replace("https://github.com/rdoeffinger/Dictionary/releases/download/v0.2-dictionaries/", "http://ffmpeg.org/~reimar/dict/");
828         Request request = new Request(Uri.parse(Build.VERSION.SDK_INT < 21 ? altUrl : downloadUrl));
829
830         String destFile;
831         try {
832             destFile = new File(new URL(downloadUrl).getPath()).getName();
833         } catch (MalformedURLException e) {
834             throw new RuntimeException("Invalid download URL!", e);
835         }
836         Log.d(LOG, "Downloading to: " + destFile);
837         request.setTitle(destFile);
838         File destFilePath = new File(application.getDictDir(), destFile);
839         destFilePath.delete();
840         try {
841             request.setDestinationUri(Uri.fromFile(destFilePath));
842         } catch (Exception e) {
843         }
844
845         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
846
847         if (downloadManager == null) {
848             String msg = getString(R.string.downloadManagerQueryFailed);
849             new AlertDialog.Builder(DictionaryManagerActivity.this).setTitle(getString(R.string.error))
850                     .setMessage(getString(R.string.downloadFailed, msg))
851                     .setNeutralButton("Close", null).show();
852             return;
853         }
854
855         try {
856             downloadManager.enqueue(request);
857         } catch (SecurityException e) {
858             request = new Request(Uri.parse(downloadUrl));
859             request.setTitle(destFile);
860             downloadManager.enqueue(request);
861         }
862         Log.w(LOG, "Download started: " + destFile);
863         downloadButton.setText("X");
864     }
865
866 }