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