X-Git-Url: http://gitweb.fperrin.net/?p=Dictionary.git;a=blobdiff_plain;f=src%2Fcom%2Fhughes%2Fandroid%2Fdictionary%2FDictionaryManagerActivity.java;h=faf507da40dca60fd84a5ab53b1f551da1c4c5e1;hp=e3e76ad72a3c2f3b6944f0603a11f55c1813580e;hb=c76660b2772122109529d3616289980a7084eeeb;hpb=668f0c4dc71677f782ac40f01415595e43a0a93c diff --git a/src/com/hughes/android/dictionary/DictionaryManagerActivity.java b/src/com/hughes/android/dictionary/DictionaryManagerActivity.java index e3e76ad..faf507d 100644 --- a/src/com/hughes/android/dictionary/DictionaryManagerActivity.java +++ b/src/com/hughes/android/dictionary/DictionaryManagerActivity.java @@ -14,8 +14,8 @@ package com.hughes.android.dictionary; +import android.Manifest; import android.app.AlertDialog; -import android.app.Dialog; import android.app.DownloadManager; import android.app.DownloadManager.Request; import android.content.BroadcastReceiver; @@ -24,69 +24,106 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; -import android.preference.PreferenceManager; +import android.support.v7.preference.PreferenceManager; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.SearchView; +import android.support.v7.widget.SearchView.OnQueryTextListener; +import android.support.v7.widget.Toolbar; +import android.text.InputType; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.CompoundButton; -import android.widget.ListView; -import android.widget.Toast; import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.FrameLayout; +import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import android.widget.ToggleButton; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.widget.SearchView; -import com.actionbarsherlock.widget.SearchView.OnQueryTextListener; import com.hughes.android.dictionary.DictionaryInfo.IndexInfo; import com.hughes.android.util.IntentLauncher; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; // Right-click: // Delete, move to top. -public class DictionaryManagerActivity extends SherlockListActivity { +public class DictionaryManagerActivity extends AppCompatActivity { + + private static final String LOG = "QuickDic"; + private static boolean blockAutoLaunch = false; + + private ListView listView; + private ListView getListView() { + if (listView == null) { + listView = (ListView)findViewById(android.R.id.list); + } + return listView; + } + private void setListAdapter(ListAdapter adapter) { + getListView().setAdapter(adapter); + } + private ListAdapter getListAdapter() { + return getListView().getAdapter(); + } + + // For DownloadManager bug workaround + private final Set finishedDownloadIds = new HashSet<>(); - static final String LOG = "QuickDic"; - static boolean blockAutoLaunch = false; + private DictionaryApplication application; - DictionaryApplication application; + private SearchView filterSearchView; + private ToggleButton showDownloadable; - SearchView filterSearchView; - ToggleButton showDownloadable; + private LinearLayout dictionariesOnDeviceHeaderRow; + private LinearLayout downloadableDictionariesHeaderRow; - LinearLayout dictionariesOnDeviceHeaderRow; - LinearLayout downloadableDictionariesHeaderRow; + private Handler uiHandler; - Handler uiHandler; - - Runnable dictionaryUpdater = new Runnable() { + private final Runnable dictionaryUpdater = new Runnable() { @Override public void run() { if (uiHandler == null) { @@ -95,154 +132,325 @@ public class DictionaryManagerActivity extends SherlockListActivity { uiHandler.post(new Runnable() { @Override public void run() { - setListAdapater(); + setMyListAdapter(); } }); } }; - - final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { + public synchronized void onReceive(Context context, Intent intent) { final String action = intent.getAction(); + if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) { + startActivity(getLaunchIntent(getApplicationContext()).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP)); + } if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { final long downloadId = intent.getLongExtra( - DownloadManager.EXTRA_DOWNLOAD_ID, 0); + DownloadManager.EXTRA_DOWNLOAD_ID, 0); + if (finishedDownloadIds.contains(downloadId)) return; // ignore double notifications final DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadId); final DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); final Cursor cursor = downloadManager.query(query); - if (!cursor.moveToFirst()) { + if (cursor == null || !cursor.moveToFirst()) { Log.e(LOG, "Couldn't find download."); return; } final String dest = cursor - .getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); + .getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); final int status = cursor - .getInt(cursor - .getColumnIndex(DownloadManager.COLUMN_STATUS)); + .getInt(cursor + .getColumnIndex(DownloadManager.COLUMN_STATUS)); if (DownloadManager.STATUS_SUCCESSFUL != status) { + final int reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON)); Log.w(LOG, - "Download failed: status=" + status + - ", reason=" + cursor.getString(cursor - .getColumnIndex(DownloadManager.COLUMN_REASON))); - Toast.makeText(context, getString(R.string.downloadFailed, dest), - Toast.LENGTH_LONG).show(); + "Download failed: status=" + status + + ", reason=" + reason); + String msg = Integer.toString(reason); + switch (reason) { + case DownloadManager.ERROR_FILE_ALREADY_EXISTS: + msg = "File exists"; + break; + case DownloadManager.ERROR_FILE_ERROR: + msg = "File error"; + break; + case DownloadManager.ERROR_INSUFFICIENT_SPACE: + msg = "Not enough space"; + break; + } + new AlertDialog.Builder(context).setTitle(getString(R.string.error)).setMessage(getString(R.string.downloadFailed, msg)).setNeutralButton("Close", null).show(); return; } - Log.w(LOG, "Download finished: " + dest); - final File localZipFile = new File(Uri.parse(dest).getPath()); + Log.w(LOG, "Download finished: " + dest + " Id: " + downloadId); + if (!isFinishing()) + Toast.makeText(context, getString(R.string.unzippingDictionary, dest), + Toast.LENGTH_LONG).show(); + + if (unzipInstall(context, Uri.parse(dest), dest, true)) { + finishedDownloadIds.add(downloadId); + Log.w(LOG, "Unzipping finished: " + dest + " Id: " + downloadId); + } + } + } + }; + private boolean unzipInstall(Context context, Uri zipUri, String dest, boolean delete) { + File localZipFile = null; + InputStream zipFileStream = null; + ZipInputStream zipFile = null; + FileOutputStream zipOut = null; + boolean result = false; + try { + if (zipUri.getScheme().equals("content")) { + zipFileStream = context.getContentResolver().openInputStream(zipUri); + localZipFile = null; + } else { + localZipFile = new File(zipUri.getPath()); try { - ZipFile zipFile = new ZipFile(localZipFile); - final ZipEntry zipEntry = zipFile.entries().nextElement(); - Log.d(LOG, "Unzipping entry: " + zipEntry.getName()); - final InputStream zipIn = zipFile.getInputStream(zipEntry); - final OutputStream zipOut = new FileOutputStream( - new File(application.getDictDir(), zipEntry.getName())); - copyStream(zipIn, zipOut); - zipFile.close(); - application.backgroundUpdateDictionaries(dictionaryUpdater); - Toast.makeText(context, getString(R.string.downloadFinished, dest), - Toast.LENGTH_LONG).show(); + zipFileStream = new FileInputStream(localZipFile); } catch (Exception e) { - Toast.makeText(context, getString(R.string.downloadFailed, dest), - Toast.LENGTH_LONG).show(); - Log.e(LOG, "Failed to unzip.", e); - } finally { - localZipFile.delete(); + if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[] {Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, 0); + return false; + } + throw e; + } + } + zipFile = new ZipInputStream(new BufferedInputStream(zipFileStream)); + ZipEntry zipEntry; + while ((zipEntry = zipFile.getNextEntry()) != null) { + // Note: this check prevents security issues like accidental path + // traversal, which unfortunately ZipInputStream has no protection against. + // So take extra care when changing it. + if (!Pattern.matches("[-A-Za-z]+\\.quickdic", zipEntry.getName())) { + Log.w(LOG, "Invalid zip entry: " + zipEntry.getName()); + continue; + } + Log.d(LOG, "Unzipping entry: " + zipEntry.getName()); + File targetFile = new File(application.getDictDir(), zipEntry.getName()); + if (targetFile.exists()) { + targetFile.renameTo(new File(targetFile.getAbsolutePath().replace(".quickdic", ".bak.quickdic"))); + targetFile = new File(application.getDictDir(), zipEntry.getName()); } + zipOut = new FileOutputStream(targetFile); + copyStream(zipFile, zipOut); } + application.backgroundUpdateDictionaries(dictionaryUpdater); + if (!isFinishing()) + Toast.makeText(context, getString(R.string.installationFinished, dest), + Toast.LENGTH_LONG).show(); + result = true; + } catch (Exception e) { + String msg = getString(R.string.unzippingFailed, dest + ": " + e.getMessage()); + File dir = application.getDictDir(); + if (!dir.canWrite() || !DictionaryApplication.checkFileCreate(dir)) { + msg = getString(R.string.notWritable, dir.getAbsolutePath()); + } + new AlertDialog.Builder(context).setTitle(getString(R.string.error)).setMessage(msg).setNeutralButton("Close", null).show(); + Log.e(LOG, "Failed to unzip.", e); + } finally { + try { + if (zipOut != null) zipOut.close(); + } catch (IOException ignored) {} + try { + if (zipFile != null) zipFile.close(); + } catch (IOException ignored) {} + try { + if (zipFileStream != null) zipFileStream.close(); + } catch (IOException ignored) {} + if (localZipFile != null && delete) //noinspection ResultOfMethodCallIgnored + localZipFile.delete(); } - }; + return result; + } - public static Intent getLaunchIntent() { - final Intent intent = new Intent(); - intent.setClassName(DictionaryManagerActivity.class.getPackage().getName(), - DictionaryManagerActivity.class.getName()); + public static Intent getLaunchIntent(Context c) { + final Intent intent = new Intent(c, DictionaryManagerActivity.class); intent.putExtra(C.CAN_AUTO_LAUNCH_DICT, false); return intent; } + private void readableCheckAndError(boolean requestPermission) { + final File dictDir = application.getDictDir(); + if (dictDir.canRead() && dictDir.canExecute()) return; + blockAutoLaunch = true; + if (requestPermission && + ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[] {Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, 0); + return; + } + blockAutoLaunch = true; + + AlertDialog.Builder builder = new AlertDialog.Builder(getListView().getContext()); + builder.setTitle(getString(R.string.error)); + builder.setMessage(getString( + R.string.unableToReadDictionaryDir, + dictDir.getAbsolutePath(), + Environment.getExternalStorageDirectory())); + builder.setNeutralButton("Close", null); + builder.create().show(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + readableCheckAndError(false); + + application.backgroundUpdateDictionaries(dictionaryUpdater); + + setMyListAdapter(); + } + @Override public void onCreate(Bundle savedInstanceState) { - setTheme(((DictionaryApplication) getApplication()).getSelectedTheme().themeId); + DictionaryApplication.INSTANCE.init(getApplicationContext()); + application = DictionaryApplication.INSTANCE; + // This must be first, otherwise the action bar doesn't get + // styled properly. + setTheme(application.getSelectedTheme().themeId); super.onCreate(savedInstanceState); Log.d(LOG, "onCreate:" + this); - application = (DictionaryApplication) getApplication(); - + setTheme(application.getSelectedTheme().themeId); + blockAutoLaunch = false; - + // UI init. setContentView(R.layout.dictionary_manager_activity); - dictionariesOnDeviceHeaderRow = (LinearLayout) LayoutInflater.from(getListView().getContext()).inflate( - R.layout.dictionary_manager_header_row_on_device, getListView(), false); + dictionariesOnDeviceHeaderRow = (LinearLayout) LayoutInflater.from( + getListView().getContext()).inflate( + R.layout.dictionary_manager_header_row_on_device, getListView(), false); - downloadableDictionariesHeaderRow = (LinearLayout) LayoutInflater.from(getListView().getContext()).inflate( - R.layout.dictionary_manager_header_row_downloadable, getListView(), false); + downloadableDictionariesHeaderRow = (LinearLayout) LayoutInflater.from( + getListView().getContext()).inflate( + R.layout.dictionary_manager_header_row_downloadable, getListView(), false); - showDownloadable = (ToggleButton) downloadableDictionariesHeaderRow.findViewById(R.id.hideDownloadable); + showDownloadable = downloadableDictionariesHeaderRow + .findViewById(R.id.hideDownloadable); showDownloadable.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - onShowLocalChanged(); + onShowDownloadableChanged(); } }); + /* + Disable version update notification, I don't maintain the text really + and I don't think it is very useful. final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final String thanksForUpdatingLatestVersion = getString(R.string.thanksForUpdatingVersion); if (!prefs.getString(C.THANKS_FOR_UPDATING_VERSION, "").equals( - thanksForUpdatingLatestVersion)) { + thanksForUpdatingLatestVersion)) { blockAutoLaunch = true; - startActivity(HtmlDisplayActivity.getWhatsNewLaunchIntent()); + startActivity(HtmlDisplayActivity.getWhatsNewLaunchIntent(getApplicationContext())); prefs.edit().putString(C.THANKS_FOR_UPDATING_VERSION, thanksForUpdatingLatestVersion) - .commit(); - } - - - registerReceiver(broadcastReceiver, new IntentFilter( - DownloadManager.ACTION_DOWNLOAD_COMPLETE)); - - setListAdapater(); + .commit(); + } + */ + IntentFilter downloadManagerIntents = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); + downloadManagerIntents.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED); + registerReceiver(broadcastReceiver, downloadManagerIntents); + + setMyListAdapter(); registerForContextMenu(getListView()); - - final File dictDir = application.getDictDir(); - if (!dictDir.canRead() || !dictDir.canExecute()) { + getListView().setItemsCanFocus(true); + + readableCheckAndError(true); + + onCreateSetupActionBar(); + + final Intent intent = getIntent(); + if (intent != null && intent.getAction() != null && + intent.getAction().equals(Intent.ACTION_VIEW)) { blockAutoLaunch = true; - - AlertDialog.Builder builder = new AlertDialog.Builder(getListView().getContext()); - builder.setTitle(getString(R.string.error)); - builder.setMessage(getString( - R.string.unableToReadDictionaryDir, - dictDir.getAbsolutePath(), - Environment.getExternalStorageDirectory())); - builder.create().show(); + Uri uri = intent.getData(); + unzipInstall(this, uri, uri.getLastPathSegment(), false); } } - + + private void onCreateSetupActionBar() { + ActionBar actionBar = getSupportActionBar(); + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayShowHomeEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(false); + + filterSearchView = new SearchView(getSupportActionBar().getThemedContext()); + filterSearchView.setIconifiedByDefault(false); + // filterSearchView.setIconified(false); // puts the magnifying glass in + // the + // wrong place. + filterSearchView.setQueryHint(getString(R.string.searchText)); + filterSearchView.setSubmitButtonEnabled(false); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT); + filterSearchView.setLayoutParams(lp); + filterSearchView.setInputType(InputType.TYPE_CLASS_TEXT); + filterSearchView.setImeOptions( + EditorInfo.IME_ACTION_DONE | + EditorInfo.IME_FLAG_NO_EXTRACT_UI | + // EditorInfo.IME_FLAG_NO_FULLSCREEN | // Requires API + // 11 + EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + filterSearchView.setOnQueryTextListener(new OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + filterSearchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String filterText) { + setMyListAdapter(); + return true; + } + }); + filterSearchView.setFocusable(true); + + actionBar.setCustomView(filterSearchView); + actionBar.setDisplayShowCustomEnabled(true); + + // Avoid wasting space on large left inset + Toolbar tb = (Toolbar)filterSearchView.getParent(); + tb.setContentInsetsRelative(0, 0); + } + @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(broadcastReceiver); } - - private static int copyStream(final InputStream in, final OutputStream out) - throws IOException { + + private static void copyStream(final InputStream ins, final FileOutputStream outs) + throws IOException { + ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 64); + FileChannel out = outs.getChannel(); int bytesRead; - final byte[] bytes = new byte[1024 * 16]; - while ((bytesRead = in.read(bytes)) != -1) { - out.write(bytes, 0, bytesRead); - } - in.close(); - out.close(); - return bytesRead; + int pos = 0; + final byte[] bytes = new byte[1024 * 64]; + do { + bytesRead = ins.read(bytes, pos, bytes.length - pos); + if (bytesRead != -1) pos += bytesRead; + if (bytesRead == -1 ? pos != 0 : 2*pos >= bytes.length) { + buf.put(bytes, 0, pos); + pos = 0; + buf.flip(); + while (buf.hasRemaining()) out.write(buf); + buf.clear(); + } + } while (bytesRead != -1); } @Override @@ -268,141 +476,127 @@ public class DictionaryManagerActivity extends SherlockListActivity { } final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - showDownloadable.setChecked(prefs.getBoolean(C.SHOW_DOWNLOADABLE, false)); + showDownloadable.setChecked(prefs.getBoolean(C.SHOW_DOWNLOADABLE, true)); if (!blockAutoLaunch && getIntent().getBooleanExtra(C.CAN_AUTO_LAUNCH_DICT, true) && prefs.contains(C.DICT_FILE) && prefs.contains(C.INDEX_SHORT_NAME)) { Log.d(LOG, "Skipping DictionaryManager, going straight to dictionary."); - startActivity(DictionaryActivity.getLaunchIntent( - new File(prefs.getString(C.DICT_FILE, "")), prefs.getString(C.INDEX_SHORT_NAME, ""), - prefs.getString(C.SEARCH_TOKEN, ""))); + startActivity(DictionaryActivity.getLaunchIntent(getApplicationContext(), + new File(prefs.getString(C.DICT_FILE, "")), + prefs.getString(C.INDEX_SHORT_NAME, ""), + "")); finish(); return; } - + // Remove the active dictionary from the prefs so we won't autolaunch // next time. - final Editor editor = prefs.edit(); - editor.remove(C.DICT_FILE); - editor.remove(C.INDEX_SHORT_NAME); - editor.remove(C.SEARCH_TOKEN); - editor.commit(); + prefs.edit().remove(C.DICT_FILE).remove(C.INDEX_SHORT_NAME).commit(); application.backgroundUpdateDictionaries(dictionaryUpdater); - setListAdapater(); + setMyListAdapter(); } @Override public boolean onCreateOptionsMenu(final Menu menu) { - MenuInflater inflater = getSupportMenuInflater(); - inflater.inflate(R.menu.dictionary_manager_options_menu, menu); - - filterSearchView = (SearchView) menu.findItem(R.id.filterText).getActionView(); - filterSearchView.setOnQueryTextListener(new OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { + if ("true".equals(Settings.System.getString(getContentResolver(), "firebase.test.lab"))) + { + return false; // testing the menu is not very interesting + } + final MenuItem sort = menu.add(getString(R.string.sortDicts)); + MenuItemCompat.setShowAsAction(sort, MenuItem.SHOW_AS_ACTION_NEVER); + sort.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(final MenuItem menuItem) { + application.sortDictionaries(); + setMyListAdapter(); return true; } - - @Override - public boolean onQueryTextChange(String filterText) { - setListAdapater(); - return true; + }); + + final MenuItem browserDownload = menu.add(getString(R.string.browserDownload)); + MenuItemCompat.setShowAsAction(browserDownload, MenuItem.SHOW_AS_ACTION_NEVER); + browserDownload.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(final MenuItem menuItem) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri + .parse("https://github.com/rdoeffinger/Dictionary/releases/v0.3-dictionaries")); + startActivity(intent); + return false; } }); - filterSearchView.setIconifiedByDefault(false); - application.onCreateGlobalOptionsMenu(this, menu); + DictionaryApplication.onCreateGlobalOptionsMenu(this, menu); return true; } @Override public void onCreateContextMenu(final ContextMenu menu, final View view, - final ContextMenuInfo menuInfo) { + final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, view, menuInfo); Log.d(LOG, "onCreateContextMenu, " + menuInfo); final AdapterContextMenuInfo adapterContextMenuInfo = - (AdapterContextMenuInfo) menuInfo; + (AdapterContextMenuInfo) menuInfo; final int position = adapterContextMenuInfo.position; final MyListAdapter.Row row = (MyListAdapter.Row) getListAdapter().getItem(position); - + if (row.dictionaryInfo == null) { return; } if (position > 0 && row.onDevice) { final android.view.MenuItem moveToTopMenuItem = - menu.add(R.string.moveToTop); + menu.add(R.string.moveToTop); moveToTopMenuItem.setOnMenuItemClickListener(new - android.view.MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(android.view.MenuItem item) { - application.moveDictionaryToTop(row.dictionaryInfo); - setListAdapater(); - return true; - } - }); + android.view.MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(android.view.MenuItem item) { + application.moveDictionaryToTop(row.dictionaryInfo); + setMyListAdapter(); + return true; + } + }); } if (row.onDevice) { final android.view.MenuItem deleteMenuItem = menu.add(R.string.deleteDictionary); deleteMenuItem - .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(android.view.MenuItem item) { - application.deleteDictionary(row.dictionaryInfo); - setListAdapater(); - return true; - } - }); + .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(android.view.MenuItem item) { + application.deleteDictionary(row.dictionaryInfo); + setMyListAdapter(); + return true; + } + }); } } - private void onShowLocalChanged() { - setListAdapater(); + private void onShowDownloadableChanged() { + setMyListAdapter(); Editor prefs = PreferenceManager.getDefaultSharedPreferences(this).edit(); prefs.putBoolean(C.SHOW_DOWNLOADABLE, showDownloadable.isChecked()); prefs.commit(); } - // private void onClick(int index) { - // final DictionaryInfo dictionaryInfo = adapter.getItem(index); - // final DictionaryInfo downloadable = - // application.getDownloadable(dictionaryInfo.uncompressedFilename); - // if - // (!application.isDictionaryOnDevice(dictionaryInfo.uncompressedFilename) - // && downloadable != null) { - // final Intent intent = getDownloadIntent(downloadable); - // startActivity(intent); - // } else { - // final Intent intent = - // DictionaryActivity.getLaunchIntent(application.getPath(dictionaryInfo.uncompressedFilename), - // 0, ""); - // startActivity(intent); - // } - // } - - class MyListAdapter extends BaseAdapter { - List dictionariesOnDevice; - List downloadableDictionaries; - + final List dictionariesOnDevice; + final List downloadableDictionaries; + class Row { - DictionaryInfo dictionaryInfo; - boolean onDevice; - - private Row(DictionaryInfo dictinoaryInfo, boolean onDevice) { - this.dictionaryInfo = dictinoaryInfo; + final DictionaryInfo dictionaryInfo; + final boolean onDevice; + + private Row(DictionaryInfo dictionaryInfo, boolean onDevice) { + this.dictionaryInfo = dictionaryInfo; this.onDevice = onDevice; } } - private MyListAdapter(final String[] filters) { dictionariesOnDevice = application.getDictionariesOnDevice(filters); if (showDownloadable.isChecked()) { @@ -423,17 +617,17 @@ public class DictionaryManagerActivity extends SherlockListActivity { return new Row(null, true); } position -= 1; - + if (position < dictionariesOnDevice.size()) { return new Row(dictionariesOnDevice.get(position), true); } position -= dictionariesOnDevice.size(); - + if (position == 0) { return new Row(null, false); } position -= 1; - + assert position < downloadableDictionaries.size(); return new Row(downloadableDictionaries.get(position), false); } @@ -443,111 +637,235 @@ public class DictionaryManagerActivity extends SherlockListActivity { return position; } + @Override + public int getViewTypeCount() { + return 3; + } + + @Override + public int getItemViewType(int position) { + final Row row = getItem(position); + if (row.dictionaryInfo == null) { + return row.onDevice ? 0 : 1; + } + assert row.dictionaryInfo.indexInfos.size() <= 2; + return 2; + } + @Override public View getView(int position, View convertView, ViewGroup parent) { - if (convertView instanceof LinearLayout && - convertView != dictionariesOnDeviceHeaderRow && - convertView != downloadableDictionariesHeaderRow) { - /* This is done to try to avoid leaking memory that used to - * happen on Android 4.0.3 */ - ((LinearLayout)convertView).removeAllViews(); + if (convertView == dictionariesOnDeviceHeaderRow || + convertView == downloadableDictionariesHeaderRow) { + return convertView; } - + final Row row = getItem(position); - - if (row.onDevice) { - if (row.dictionaryInfo == null) { - return dictionariesOnDeviceHeaderRow; - } - return createDictionaryRow(row.dictionaryInfo, parent, true); - } - + if (row.dictionaryInfo == null) { - return downloadableDictionariesHeaderRow; + assert convertView == null; + return row.onDevice ? dictionariesOnDeviceHeaderRow : downloadableDictionariesHeaderRow; } - return createDictionaryRow(row.dictionaryInfo, parent, false); + return createDictionaryRow(row.dictionaryInfo, parent, convertView, row.onDevice); } - + } - - private void setListAdapater() { - final String filter = filterSearchView == null ? "" : filterSearchView.getQuery().toString(); + + private void setMyListAdapter() { + final String filter = filterSearchView == null ? "" : filterSearchView.getQuery() + .toString(); final String[] filters = filter.trim().toLowerCase().split("(\\s|-)+"); setListAdapter(new MyListAdapter(filters)); } - private View createDictionaryRow(final DictionaryInfo dictionaryInfo, - final ViewGroup parent, final boolean canLaunch) { - - View row = LayoutInflater.from(parent.getContext()).inflate( - R.layout.dictionary_manager_row, parent, false); - final TextView name = (TextView) row.findViewById(R.id.dictionaryName); - final TextView details = (TextView) row.findViewById(R.id.dictionaryDetails); + private boolean isDownloadActive(String downloadUrl, boolean cancel) { + DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + final DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING); + final Cursor cursor = downloadManager.query(query); + + // Due to a bug, cursor is null instead of empty when + // the download manager is disabled. + if (cursor == null) { + if (cancel) { + String msg = getString(R.string.downloadManagerQueryFailed); + new AlertDialog.Builder(this).setTitle(getString(R.string.error)) + .setMessage(getString(R.string.downloadFailed, msg)) + .setNeutralButton("Close", null).show(); + } + return cancel; + } + + String destFile; + try { + destFile = new File(new URL(downloadUrl).getPath()).getName(); + } catch (MalformedURLException e) { + throw new RuntimeException("Invalid download URL!", e); + } + while (cursor.moveToNext()) { + if (downloadUrl.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI)))) + break; + if (destFile.equals(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)))) + break; + } + boolean active = !cursor.isAfterLast(); + if (active && cancel) { + long downloadId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID)); + finishedDownloadIds.add(downloadId); + downloadManager.remove(downloadId); + } + cursor.close(); + return active; + } + + private View createDictionaryRow(final DictionaryInfo dictionaryInfo, + final ViewGroup parent, View row, boolean canLaunch) { + + if (row == null) { + row = LayoutInflater.from(parent.getContext()).inflate( + R.layout.dictionary_manager_row, parent, false); + } + final TextView name = row.findViewById(R.id.dictionaryName); + final TextView details = row.findViewById(R.id.dictionaryDetails); name.setText(application.getDictionaryName(dictionaryInfo.uncompressedFilename)); final boolean updateAvailable = application.updateAvailable(dictionaryInfo); - final Button downloadButton = (Button) row.findViewById(R.id.downloadButton); - if (!canLaunch || updateAvailable) { - downloadButton.setText(getString(R.string.downloadButton, application.getDownloadable(dictionaryInfo.uncompressedFilename).zipBytes / 1024.0 / 1024.0)); + final Button downloadButton = row.findViewById(R.id.downloadButton); + final DictionaryInfo downloadable = application.getDownloadable(dictionaryInfo.uncompressedFilename); + boolean broken = false; + if (!dictionaryInfo.isValid()) { + broken = true; + canLaunch = false; + } + if (downloadable != null && (!canLaunch || updateAvailable)) { + downloadButton + .setText(getString( + R.string.downloadButton, + downloadable.zipBytes / 1024.0 / 1024.0)); downloadButton.setMinWidth(application.languageButtonPixels * 3 / 2); downloadButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { - downloadDictionary(dictionaryInfo); + downloadDictionary(downloadable.downloadUrl, downloadable.zipBytes, downloadButton); } }); + downloadButton.setVisibility(View.VISIBLE); + + if (isDownloadActive(downloadable.downloadUrl, false)) + downloadButton.setText("X"); } else { - downloadButton.setVisibility(View.INVISIBLE); + downloadButton.setVisibility(View.GONE); } - LinearLayout buttons = (LinearLayout) row.findViewById(R.id.dictionaryLauncherButtons); - final List sortedIndexInfos = application.sortedIndexInfos(dictionaryInfo.indexInfos); + LinearLayout buttons = row.findViewById(R.id.dictionaryLauncherButtons); + + final List sortedIndexInfos = application + .sortedIndexInfos(dictionaryInfo.indexInfos); final StringBuilder builder = new StringBuilder(); if (updateAvailable) { - builder.append(getString(R.string.updateButton)); + builder.append(getString(R.string.updateAvailable)); } - for (IndexInfo indexInfo : sortedIndexInfos) { - final View button = application.createButton(buttons.getContext(), dictionaryInfo, indexInfo); - buttons.addView(button); - + assert buttons.getChildCount() == 4; + for (int i = 0; i < 2; i++) { + final Button textButton = (Button)buttons.getChildAt(2*i); + final ImageButton imageButton = (ImageButton)buttons.getChildAt(2*i + 1); + if (i >= sortedIndexInfos.size()) { + textButton.setVisibility(View.GONE); + imageButton.setVisibility(View.GONE); + continue; + } + final IndexInfo indexInfo = sortedIndexInfos.get(i); + final View button = IsoUtils.INSTANCE.setupButton(textButton, imageButton, + indexInfo); + if (canLaunch) { button.setOnClickListener( - new IntentLauncher(buttons.getContext(), - DictionaryActivity.getLaunchIntent( - application.getPath(dictionaryInfo.uncompressedFilename), - indexInfo.shortName, ""))); + new IntentLauncher(buttons.getContext(), + DictionaryActivity.getLaunchIntent(getApplicationContext(), + application.getPath(dictionaryInfo.uncompressedFilename), + indexInfo.shortName, ""))); - } else { - button.setEnabled(false); } + button.setEnabled(canLaunch); + button.setFocusable(canLaunch); if (builder.length() != 0) { builder.append("; "); } - builder.append(getString(R.string.indexInfo, indexInfo.shortName, indexInfo.mainTokenCount)); + builder.append(getString(R.string.indexInfo, indexInfo.shortName, + indexInfo.mainTokenCount)); + } + builder.append("; "); + builder.append(getString(R.string.downloadButton, dictionaryInfo.uncompressedBytes / 1024.0 / 1024.0)); + if (broken) { + name.setText("Broken: " + application.getDictionaryName(dictionaryInfo.uncompressedFilename)); + builder.append("; Cannot be used, redownload, check hardware/file system"); } details.setText(builder.toString()); - - row.setClickable(true); - row.setFocusable(true); - row.setLongClickable(true); + + if (canLaunch) { + row.setOnClickListener(new IntentLauncher(parent.getContext(), + DictionaryActivity.getLaunchIntent(getApplicationContext(), + application.getPath(dictionaryInfo.uncompressedFilename), + dictionaryInfo.indexInfos.get(0).shortName, ""))); + // do not setFocusable, for keyboard navigation + // offering only the index buttons is better. + } + row.setClickable(canLaunch); + // Allow deleting, even if we cannot open + row.setLongClickable(broken || canLaunch); row.setBackgroundResource(android.R.drawable.menuitem_background); - + return row; } - - private void downloadDictionary(final DictionaryInfo dictionaryInfo) { - DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); - Request request = new Request( - Uri.parse(dictionaryInfo.downloadUrl)); + + private synchronized void downloadDictionary(final String downloadUrl, long bytes, Button downloadButton) { + if (isDownloadActive(downloadUrl, true)) { + downloadButton + .setText(getString( + R.string.downloadButton, + bytes / 1024.0 / 1024.0)); + return; + } + // API 19 and earlier have issues with github URLs, both http and https. + // Really old (~API 10) DownloadManager cannot handle https at all. + // Work around both with in one. + String altUrl = downloadUrl.replace("https://github.com/rdoeffinger/Dictionary/releases/download/v0.2-dictionaries/", "http://ffmpeg.org/~reimar/dict/"); + altUrl = altUrl.replace("https://github.com/rdoeffinger/Dictionary/releases/download/v0.3-dictionaries/", "http://ffmpeg.org/~reimar/dict/"); + Request request = new Request(Uri.parse(Build.VERSION.SDK_INT < 21 ? altUrl : downloadUrl)); + + String destFile; try { - final String destFile = new File(new URL(dictionaryInfo.downloadUrl).getFile()).getName(); - Log.d(LOG, "Downloading to: " + destFile); - - request.setDestinationUri(Uri.fromFile(new File(Environment.getExternalStorageDirectory(), destFile))); + destFile = new File(new URL(downloadUrl).getPath()).getName(); } catch (MalformedURLException e) { - throw new RuntimeException(e); + throw new RuntimeException("Invalid download URL!", e); + } + Log.d(LOG, "Downloading to: " + destFile); + request.setTitle(destFile); + File destFilePath = new File(application.getDictDir(), destFile); + destFilePath.delete(); + try { + request.setDestinationUri(Uri.fromFile(destFilePath)); + } catch (Exception e) { + } + + DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + + if (downloadManager == null) { + String msg = getString(R.string.downloadManagerQueryFailed); + new AlertDialog.Builder(this).setTitle(getString(R.string.error)) + .setMessage(getString(R.string.downloadFailed, msg)) + .setNeutralButton("Close", null).show(); + return; + } + + try { + downloadManager.enqueue(request); + } catch (SecurityException e) { + request = new Request(Uri.parse(downloadUrl)); + request.setTitle(destFile); + downloadManager.enqueue(request); } - downloadManager.enqueue(request); + Log.w(LOG, "Download started: " + destFile); + downloadButton.setText("X"); } }