]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryManagerActivity.java
Switch to app compat preferences.
[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.support.v7.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(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         /*
350         Disable version update notification, I don't maintain the text really
351         and I don't think it is very useful.
352         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
353         final String thanksForUpdatingLatestVersion = getString(R.string.thanksForUpdatingVersion);
354         if (!prefs.getString(C.THANKS_FOR_UPDATING_VERSION, "").equals(
355                     thanksForUpdatingLatestVersion)) {
356             blockAutoLaunch = true;
357             startActivity(HtmlDisplayActivity.getWhatsNewLaunchIntent(getApplicationContext()));
358             prefs.edit().putString(C.THANKS_FOR_UPDATING_VERSION, thanksForUpdatingLatestVersion)
359             .commit();
360         }
361         */
362         IntentFilter downloadManagerIntents = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
363         downloadManagerIntents.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED);
364         registerReceiver(broadcastReceiver, downloadManagerIntents);
365
366         setMyListAdapter();
367         registerForContextMenu(getListView());
368         getListView().setItemsCanFocus(true);
369
370         readableCheckAndError(true);
371
372         onCreateSetupActionBar();
373
374         final Intent intent = getIntent();
375         if (intent != null && intent.getAction() != null &&
376             intent.getAction().equals(Intent.ACTION_VIEW)) {
377             blockAutoLaunch = true;
378             Uri uri = intent.getData();
379             unzipInstall(this, uri, uri.getLastPathSegment(), false);
380         }
381     }
382
383     private void onCreateSetupActionBar() {
384         ActionBar actionBar = getSupportActionBar();
385         actionBar.setDisplayShowTitleEnabled(false);
386         actionBar.setDisplayShowHomeEnabled(false);
387         actionBar.setDisplayHomeAsUpEnabled(false);
388
389         filterSearchView = new SearchView(getSupportActionBar().getThemedContext());
390         filterSearchView.setIconifiedByDefault(false);
391         // filterSearchView.setIconified(false); // puts the magnifying glass in
392         // the
393         // wrong place.
394         filterSearchView.setQueryHint(getString(R.string.searchText));
395         filterSearchView.setSubmitButtonEnabled(false);
396         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
397                 FrameLayout.LayoutParams.WRAP_CONTENT);
398         filterSearchView.setLayoutParams(lp);
399         filterSearchView.setInputType(InputType.TYPE_CLASS_TEXT);
400         filterSearchView.setImeOptions(
401             EditorInfo.IME_ACTION_DONE |
402             EditorInfo.IME_FLAG_NO_EXTRACT_UI |
403             // EditorInfo.IME_FLAG_NO_FULLSCREEN | // Requires API
404             // 11
405             EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
406
407         filterSearchView.setOnQueryTextListener(new OnQueryTextListener() {
408             @Override
409             public boolean onQueryTextSubmit(String query) {
410                 filterSearchView.clearFocus();
411                 return false;
412             }
413
414             @Override
415             public boolean onQueryTextChange(String filterText) {
416                 setMyListAdapter();
417                 return true;
418             }
419         });
420         filterSearchView.setFocusable(true);
421
422         actionBar.setCustomView(filterSearchView);
423         actionBar.setDisplayShowCustomEnabled(true);
424
425         // Avoid wasting space on large left inset
426         Toolbar tb = (Toolbar)filterSearchView.getParent();
427         tb.setContentInsetsRelative(0, 0);
428     }
429
430     @Override
431     public void onDestroy() {
432         super.onDestroy();
433         unregisterReceiver(broadcastReceiver);
434     }
435
436     private static void copyStream(final InputStream ins, final FileOutputStream outs)
437     throws IOException {
438         ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 64);
439         FileChannel out = outs.getChannel();
440         int bytesRead;
441         int pos = 0;
442         final byte[] bytes = new byte[1024 * 64];
443         do {
444             bytesRead = ins.read(bytes, pos, bytes.length - pos);
445             if (bytesRead != -1) pos += bytesRead;
446             if (bytesRead == -1 ? pos != 0 : 2*pos >= bytes.length) {
447                 buf.put(bytes, 0, pos);
448                 pos = 0;
449                 buf.flip();
450                 while (buf.hasRemaining()) out.write(buf);
451                 buf.clear();
452             }
453         } while (bytesRead != -1);
454     }
455
456     @Override
457     protected void onStart() {
458         super.onStart();
459         uiHandler = new Handler();
460     }
461
462     @Override
463     protected void onStop() {
464         super.onStop();
465         uiHandler = null;
466     }
467
468     @Override
469     protected void onResume() {
470         super.onResume();
471
472         if (PreferenceActivity.prefsMightHaveChanged) {
473             PreferenceActivity.prefsMightHaveChanged = false;
474             finish();
475             startActivity(getIntent());
476         }
477
478         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
479         showDownloadable.setChecked(prefs.getBoolean(C.SHOW_DOWNLOADABLE, true));
480
481         if (!blockAutoLaunch &&
482                 getIntent().getBooleanExtra(C.CAN_AUTO_LAUNCH_DICT, true) &&
483                 prefs.contains(C.DICT_FILE) &&
484                 prefs.contains(C.INDEX_SHORT_NAME)) {
485             Log.d(LOG, "Skipping DictionaryManager, going straight to dictionary.");
486             startActivity(DictionaryActivity.getLaunchIntent(getApplicationContext(),
487                           new File(prefs.getString(C.DICT_FILE, "")),
488                           prefs.getString(C.INDEX_SHORT_NAME, ""),
489                           ""));
490             finish();
491             return;
492         }
493
494         // Remove the active dictionary from the prefs so we won't autolaunch
495         // next time.
496         prefs.edit().remove(C.DICT_FILE).remove(C.INDEX_SHORT_NAME).commit();
497
498         application.backgroundUpdateDictionaries(dictionaryUpdater);
499
500         setMyListAdapter();
501     }
502
503     @Override
504     public boolean onCreateOptionsMenu(final Menu menu) {
505         if ("true".equals(Settings.System.getString(getContentResolver(), "firebase.test.lab")))
506         {
507             return false; // testing the menu is not very interesting
508         }
509         final MenuItem sort = menu.add(getString(R.string.sortDicts));
510         MenuItemCompat.setShowAsAction(sort, MenuItem.SHOW_AS_ACTION_NEVER);
511         sort.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
512             public boolean onMenuItemClick(final MenuItem menuItem) {
513                 application.sortDictionaries();
514                 setMyListAdapter();
515                 return true;
516             }
517         });
518
519         final MenuItem browserDownload = menu.add(getString(R.string.browserDownload));
520         MenuItemCompat.setShowAsAction(browserDownload, MenuItem.SHOW_AS_ACTION_NEVER);
521         browserDownload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
522             public boolean onMenuItemClick(final MenuItem menuItem) {
523                 final Intent intent = new Intent(Intent.ACTION_VIEW);
524                 intent.setData(Uri
525                                .parse("https://github.com/rdoeffinger/Dictionary/releases/v0.3-dictionaries"));
526                 startActivity(intent);
527                 return false;
528             }
529         });
530
531         DictionaryApplication.onCreateGlobalOptionsMenu(this, menu);
532         return true;
533     }
534
535     @Override
536     public void onCreateContextMenu(final ContextMenu menu, final View view,
537                                     final ContextMenuInfo menuInfo) {
538         super.onCreateContextMenu(menu, view, menuInfo);
539         Log.d(LOG, "onCreateContextMenu, " + menuInfo);
540
541         final AdapterContextMenuInfo adapterContextMenuInfo =
542             (AdapterContextMenuInfo) menuInfo;
543         final int position = adapterContextMenuInfo.position;
544         final MyListAdapter.Row row = (MyListAdapter.Row) getListAdapter().getItem(position);
545
546         if (row.dictionaryInfo == null) {
547             return;
548         }
549
550         if (position > 0 && row.onDevice) {
551             final android.view.MenuItem moveToTopMenuItem =
552                 menu.add(R.string.moveToTop);
553             moveToTopMenuItem.setOnMenuItemClickListener(new
554             android.view.MenuItem.OnMenuItemClickListener() {
555                 @Override
556                 public boolean onMenuItemClick(android.view.MenuItem item) {
557                     application.moveDictionaryToTop(row.dictionaryInfo);
558                     setMyListAdapter();
559                     return true;
560                 }
561             });
562         }
563
564         if (row.onDevice) {
565             final android.view.MenuItem deleteMenuItem = menu.add(R.string.deleteDictionary);
566             deleteMenuItem
567             .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
568                 @Override
569                 public boolean onMenuItemClick(android.view.MenuItem item) {
570                     application.deleteDictionary(row.dictionaryInfo);
571                     setMyListAdapter();
572                     return true;
573                 }
574             });
575         }
576     }
577
578     private void onShowDownloadableChanged() {
579         setMyListAdapter();
580         Editor prefs = PreferenceManager.getDefaultSharedPreferences(this).edit();
581         prefs.putBoolean(C.SHOW_DOWNLOADABLE, showDownloadable.isChecked());
582         prefs.commit();
583     }
584
585     class MyListAdapter extends BaseAdapter {
586
587         final List<DictionaryInfo> dictionariesOnDevice;
588         final List<DictionaryInfo> downloadableDictionaries;
589
590         class Row {
591             final DictionaryInfo dictionaryInfo;
592             final boolean onDevice;
593
594             private Row(DictionaryInfo dictionaryInfo, boolean onDevice) {
595                 this.dictionaryInfo = dictionaryInfo;
596                 this.onDevice = onDevice;
597             }
598         }
599
600         private MyListAdapter(final String[] filters) {
601             dictionariesOnDevice = application.getDictionariesOnDevice(filters);
602             if (showDownloadable.isChecked()) {
603                 downloadableDictionaries = application.getDownloadableDictionaries(filters);
604             } else {
605                 downloadableDictionaries = Collections.emptyList();
606             }
607         }
608
609         @Override
610         public int getCount() {
611             return 2 + dictionariesOnDevice.size() + downloadableDictionaries.size();
612         }
613
614         @Override
615         public Row getItem(int position) {
616             if (position == 0) {
617                 return new Row(null, true);
618             }
619             position -= 1;
620
621             if (position < dictionariesOnDevice.size()) {
622                 return new Row(dictionariesOnDevice.get(position), true);
623             }
624             position -= dictionariesOnDevice.size();
625
626             if (position == 0) {
627                 return new Row(null, false);
628             }
629             position -= 1;
630
631             assert position < downloadableDictionaries.size();
632             return new Row(downloadableDictionaries.get(position), false);
633         }
634
635         @Override
636         public long getItemId(int position) {
637             return position;
638         }
639
640         @Override
641         public int getViewTypeCount() {
642             return 3;
643         }
644
645         @Override
646         public int getItemViewType(int position) {
647             final Row row = getItem(position);
648             if (row.dictionaryInfo == null) {
649                 return row.onDevice ? 0 : 1;
650             }
651             assert row.dictionaryInfo.indexInfos.size() <= 2;
652             return 2;
653         }
654
655         @Override
656         public View getView(int position, View convertView, ViewGroup parent) {
657             if (convertView == dictionariesOnDeviceHeaderRow ||
658                 convertView == downloadableDictionariesHeaderRow) {
659                 return convertView;
660             }
661
662             final Row row = getItem(position);
663
664             if (row.dictionaryInfo == null) {
665                 assert convertView == null;
666                 return row.onDevice ? dictionariesOnDeviceHeaderRow : downloadableDictionariesHeaderRow;
667             }
668             return createDictionaryRow(row.dictionaryInfo, parent, convertView, row.onDevice);
669         }
670
671     }
672
673     private void setMyListAdapter() {
674         final String filter = filterSearchView == null ? "" : filterSearchView.getQuery()
675                               .toString();
676         final String[] filters = filter.trim().toLowerCase().split("(\\s|-)+");
677         setListAdapter(new MyListAdapter(filters));
678     }
679
680     private boolean isDownloadActive(String downloadUrl, boolean cancel) {
681         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
682         final DownloadManager.Query query = new DownloadManager.Query();
683         query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING);
684         final Cursor cursor = downloadManager.query(query);
685
686         // Due to a bug, cursor is null instead of empty when
687         // the download manager is disabled.
688         if (cursor == null) {
689             if (cancel) {
690                 String msg = getString(R.string.downloadManagerQueryFailed);
691                 new AlertDialog.Builder(this).setTitle(getString(R.string.error))
692                 .setMessage(getString(R.string.downloadFailed, msg))
693                 .setNeutralButton("Close", null).show();
694             }
695             return cancel;
696         }
697
698         String destFile;
699         try {
700             destFile = new File(new URL(downloadUrl).getPath()).getName();
701         } catch (MalformedURLException e) {
702             throw new RuntimeException("Invalid download URL!", e);
703         }
704         while (cursor.moveToNext()) {
705             if (downloadUrl.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI))))
706                 break;
707             if (destFile.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))))
708                 break;
709         }
710         boolean active = !cursor.isAfterLast();
711         if (active && cancel) {
712             long downloadId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
713             finishedDownloadIds.add(downloadId);
714             downloadManager.remove(downloadId);
715         }
716         cursor.close();
717         return active;
718     }
719
720     private View createDictionaryRow(final DictionaryInfo dictionaryInfo,
721                                      final ViewGroup parent, View row, boolean canLaunch) {
722
723         if (row == null) {
724             row = LayoutInflater.from(parent.getContext()).inflate(
725                        R.layout.dictionary_manager_row, parent, false);
726         }
727         final TextView name = row.findViewById(R.id.dictionaryName);
728         final TextView details = row.findViewById(R.id.dictionaryDetails);
729         name.setText(application.getDictionaryName(dictionaryInfo.uncompressedFilename));
730
731         final boolean updateAvailable = application.updateAvailable(dictionaryInfo);
732         final Button downloadButton = row.findViewById(R.id.downloadButton);
733         final DictionaryInfo downloadable = application.getDownloadable(dictionaryInfo.uncompressedFilename);
734         boolean broken = false;
735         if (!dictionaryInfo.isValid()) {
736             broken = true;
737             canLaunch = false;
738         }
739         if (downloadable != null && (!canLaunch || updateAvailable)) {
740             downloadButton
741             .setText(getString(
742                          R.string.downloadButton,
743                          downloadable.zipBytes / 1024.0 / 1024.0));
744             downloadButton.setMinWidth(application.languageButtonPixels * 3 / 2);
745             downloadButton.setOnClickListener(new OnClickListener() {
746                 @Override
747                 public void onClick(View arg0) {
748                     downloadDictionary(downloadable.downloadUrl, downloadable.zipBytes, downloadButton);
749                 }
750             });
751             downloadButton.setVisibility(View.VISIBLE);
752
753             if (isDownloadActive(downloadable.downloadUrl, false))
754                 downloadButton.setText("X");
755         } else {
756             downloadButton.setVisibility(View.GONE);
757         }
758
759         LinearLayout buttons = row.findViewById(R.id.dictionaryLauncherButtons);
760
761         final List<IndexInfo> sortedIndexInfos = application
762                 .sortedIndexInfos(dictionaryInfo.indexInfos);
763         final StringBuilder builder = new StringBuilder();
764         if (updateAvailable) {
765             builder.append(getString(R.string.updateAvailable));
766         }
767         assert buttons.getChildCount() == 4;
768         for (int i = 0; i < 2; i++) {
769             final Button textButton = (Button)buttons.getChildAt(2*i);
770             final ImageButton imageButton = (ImageButton)buttons.getChildAt(2*i + 1);
771             if (i >= sortedIndexInfos.size()) {
772                 textButton.setVisibility(View.GONE);
773                 imageButton.setVisibility(View.GONE);
774                 continue;
775             }
776             final IndexInfo indexInfo = sortedIndexInfos.get(i);
777             final View button = IsoUtils.INSTANCE.setupButton(textButton, imageButton,
778                     indexInfo);
779
780             if (canLaunch) {
781                 button.setOnClickListener(
782                     new IntentLauncher(buttons.getContext(),
783                                        DictionaryActivity.getLaunchIntent(getApplicationContext(),
784                                                application.getPath(dictionaryInfo.uncompressedFilename),
785                                                indexInfo.shortName, "")));
786
787             }
788             button.setEnabled(canLaunch);
789             button.setFocusable(canLaunch);
790             if (builder.length() != 0) {
791                 builder.append("; ");
792             }
793             builder.append(getString(R.string.indexInfo, indexInfo.shortName,
794                                      indexInfo.mainTokenCount));
795         }
796         builder.append("; ");
797         builder.append(getString(R.string.downloadButton, dictionaryInfo.uncompressedBytes / 1024.0 / 1024.0));
798         if (broken) {
799             name.setText("Broken: " + application.getDictionaryName(dictionaryInfo.uncompressedFilename));
800             builder.append("; Cannot be used, redownload, check hardware/file system");
801         }
802         details.setText(builder.toString());
803
804         if (canLaunch) {
805             row.setOnClickListener(new IntentLauncher(parent.getContext(),
806                                    DictionaryActivity.getLaunchIntent(getApplicationContext(),
807                                            application.getPath(dictionaryInfo.uncompressedFilename),
808                                            dictionaryInfo.indexInfos.get(0).shortName, "")));
809             // do not setFocusable, for keyboard navigation
810             // offering only the index buttons is better.
811         }
812         row.setClickable(canLaunch);
813         // Allow deleting, even if we cannot open
814         row.setLongClickable(broken || canLaunch);
815         row.setBackgroundResource(android.R.drawable.menuitem_background);
816
817         return row;
818     }
819
820     private synchronized void downloadDictionary(final String downloadUrl, long bytes, Button downloadButton) {
821         if (isDownloadActive(downloadUrl, true)) {
822             downloadButton
823             .setText(getString(
824                          R.string.downloadButton,
825                          bytes / 1024.0 / 1024.0));
826             return;
827         }
828         // API 19 and earlier have issues with github URLs, both http and https.
829         // Really old (~API 10) DownloadManager cannot handle https at all.
830         // Work around both with in one.
831         String altUrl = downloadUrl.replace("https://github.com/rdoeffinger/Dictionary/releases/download/v0.2-dictionaries/", "http://ffmpeg.org/~reimar/dict/");
832         altUrl = altUrl.replace("https://github.com/rdoeffinger/Dictionary/releases/download/v0.3-dictionaries/", "http://ffmpeg.org/~reimar/dict/");
833         Request request = new Request(Uri.parse(Build.VERSION.SDK_INT < 21 ? altUrl : downloadUrl));
834
835         String destFile;
836         try {
837             destFile = new File(new URL(downloadUrl).getPath()).getName();
838         } catch (MalformedURLException e) {
839             throw new RuntimeException("Invalid download URL!", e);
840         }
841         Log.d(LOG, "Downloading to: " + destFile);
842         request.setTitle(destFile);
843         File destFilePath = new File(application.getDictDir(), destFile);
844         destFilePath.delete();
845         try {
846             request.setDestinationUri(Uri.fromFile(destFilePath));
847         } catch (Exception e) {
848         }
849
850         DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
851
852         if (downloadManager == null) {
853             String msg = getString(R.string.downloadManagerQueryFailed);
854             new AlertDialog.Builder(this).setTitle(getString(R.string.error))
855                     .setMessage(getString(R.string.downloadFailed, msg))
856                     .setNeutralButton("Close", null).show();
857             return;
858         }
859
860         try {
861             downloadManager.enqueue(request);
862         } catch (SecurityException e) {
863             request = new Request(Uri.parse(downloadUrl));
864             request.setTitle(destFile);
865             downloadManager.enqueue(request);
866         }
867         Log.w(LOG, "Download started: " + destFile);
868         downloadButton.setText("X");
869     }
870
871 }