]> gitweb.fperrin.net Git - Dictionary.git/blobdiff - src/com/hughes/android/dictionary/DictionaryActivity.java
Avoid inheriting from Application.
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
index 28c5b6f2fb6d2d0b2a38b580f386900ddda9c75c..008b57de6c1ef6869612267e22177b1e79829a63 100644 (file)
@@ -20,6 +20,7 @@ import android.app.SearchManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.graphics.Color;
 import android.graphics.Typeface;
 import android.net.Uri;
@@ -41,6 +42,7 @@ import android.text.Spannable;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.StyleSpan;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.ContextMenu;
@@ -108,6 +110,7 @@ import java.util.Locale;
 import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -135,7 +138,7 @@ public class DictionaryActivity extends ActionBarActivity {
 
     final Handler uiHandler = new Handler();
 
-    private final Executor searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+    private final ExecutorService searchExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
         @Override
         public Thread newThread(Runnable r) {
             return new Thread(r, "searchExecutor");
@@ -193,11 +196,12 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     public static Intent getLaunchIntent(Context c, final File dictFile, final String indexShortName,
-            final String searchToken) {
+                                         final String searchToken) {
         final Intent intent = new Intent(c, DictionaryActivity.class);
         intent.putExtra(C.DICT_FILE, dictFile.getPath());
         intent.putExtra(C.INDEX_SHORT_NAME, indexShortName);
         intent.putExtra(C.SEARCH_TOKEN, searchToken);
+        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
         return intent;
     }
 
@@ -209,11 +213,9 @@ public class DictionaryActivity extends ActionBarActivity {
         outState.putString(C.SEARCH_TOKEN, searchView.getQuery().toString());
     }
 
-    private int getMatchLen(String search, Index.IndexEntry e)
-    {
+    private int getMatchLen(String search, Index.IndexEntry e) {
         if (e == null) return 0;
-        for (int i = 0; i < search.length(); ++i)
-        {
+        for (int i = 0; i < search.length(); ++i) {
             String a = search.substring(0, i + 1);
             String b = e.token.substring(0, i + 1);
             if (!a.equalsIgnoreCase(b))
@@ -224,10 +226,12 @@ public class DictionaryActivity extends ActionBarActivity {
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        DictionaryApplication.INSTANCE.init(getApplicationContext());
+        application = DictionaryApplication.INSTANCE;
         // This needs to be before super.onCreate, otherwise ActionbarSherlock
         // doesn't makes the background of the actionbar white when you're
         // in the dark theme.
-        setTheme(((DictionaryApplication) getApplication()).getSelectedTheme().themeId);
+        setTheme(application.getSelectedTheme().themeId);
 
         Log.d(LOG, "onCreate:" + this);
         super.onCreate(savedInstanceState);
@@ -239,7 +243,6 @@ public class DictionaryActivity extends ActionBarActivity {
 
         setContentView(R.layout.dictionary_activity);
 
-        application = (DictionaryApplication) getApplication();
         theme = application.getSelectedTheme();
         textColorFg = getResources().getColor(theme.tokenRowFgColor);
 
@@ -252,8 +255,7 @@ public class DictionaryActivity extends ActionBarActivity {
          *         -> language in which the phrase is written to -> to which
          *         language shall be translated
          */
-        if (intentAction != null && intentAction.equals("com.hughes.action.ACTION_SEARCH_DICT"))
-        {
+        if (intentAction != null && intentAction.equals("com.hughes.action.ACTION_SEARCH_DICT")) {
             String query = intent.getStringExtra(SearchManager.QUERY);
             String from = intent.getStringExtra("from");
             if (from != null)
@@ -261,42 +263,35 @@ public class DictionaryActivity extends ActionBarActivity {
             String to = intent.getStringExtra("to");
             if (to != null)
                 to = to.toLowerCase(Locale.US);
-            if (query != null)
-            {
+            if (query != null) {
                 getIntent().putExtra(C.SEARCH_TOKEN, query);
             }
-            if (intent.getStringExtra(C.DICT_FILE) == null && (from != null || to != null))
-            {
+            if (intent.getStringExtra(C.DICT_FILE) == null && (from != null || to != null)) {
                 Log.d(LOG, "DictSearch: from: " + from + " to " + to);
                 List<DictionaryInfo> dicts = application.getDictionariesOnDevice(null);
-                for (DictionaryInfo info : dicts)
-                {
+                for (DictionaryInfo info : dicts) {
                     boolean hasFrom = from == null;
                     boolean hasTo = to == null;
-                    for (IndexInfo index : info.indexInfos)
-                    {
+                    for (IndexInfo index : info.indexInfos) {
                         if (!hasFrom && index.shortName.toLowerCase(Locale.US).equals(from))
                             hasFrom = true;
                         if (!hasTo && index.shortName.toLowerCase(Locale.US).equals(to))
                             hasTo = true;
                     }
-                    if (hasFrom && hasTo)
-                    {
-                        if (from != null)
-                        {
+                    if (hasFrom && hasTo) {
+                        if (from != null) {
                             int which_index = 0;
-                            for (; which_index < info.indexInfos.size(); ++which_index)
-                            {
+                            for (; which_index < info.indexInfos.size(); ++which_index) {
                                 if (info.indexInfos.get(which_index).shortName.toLowerCase(
-                                        Locale.US).equals(from))
+                                            Locale.US).equals(from))
                                     break;
                             }
                             intent.putExtra(C.INDEX_SHORT_NAME,
-                                    info.indexInfos.get(which_index).shortName);
+                                            info.indexInfos.get(which_index).shortName);
 
                         }
                         intent.putExtra(C.DICT_FILE, application.getPath(info.uncompressedFilename)
-                                .toString());
+                                        .toString());
                         break;
                     }
                 }
@@ -308,14 +303,12 @@ public class DictionaryActivity extends ActionBarActivity {
          *         simple query Arguments follow from android standard (see
          *         documentation)
          */
-        if (intentAction != null && intentAction.equals(Intent.ACTION_SEARCH))
-        {
+        if (intentAction != null && intentAction.equals(Intent.ACTION_SEARCH)) {
             String query = intent.getStringExtra(SearchManager.QUERY);
             if (query != null)
                 getIntent().putExtra(C.SEARCH_TOKEN, query);
         }
-        if (intentAction != null && intentAction.equals(Intent.ACTION_SEND))
-        {
+        if (intentAction != null && intentAction.equals(Intent.ACTION_SEND)) {
             String query = intent.getStringExtra(Intent.EXTRA_TEXT);
             if (query != null)
                 getIntent().putExtra(C.SEARCH_TOKEN, query);
@@ -335,23 +328,20 @@ public class DictionaryActivity extends ActionBarActivity {
          *         fail (no default dictionary specified), show a toast and
          *         abort.
          */
-        if (intent.getStringExtra(C.DICT_FILE) == null)
-        {
+        if (intent.getStringExtra(C.DICT_FILE) == null) {
             String dictfile = prefs.getString(getString(R.string.defaultDicKey), null);
             if (dictfile != null)
                 intent.putExtra(C.DICT_FILE, application.getPath(dictfile).toString());
         }
         String dictFilename = intent.getStringExtra(C.DICT_FILE);
-        if (dictFilename == null && intent.getStringExtra(C.SEARCH_TOKEN) != null)
-        {
+        if (dictFilename == null && intent.getStringExtra(C.SEARCH_TOKEN) != null) {
             final List<DictionaryInfo> dics = application.getDictionariesOnDevice(null);
             final String search = intent.getStringExtra(C.SEARCH_TOKEN);
             String bestFname = null;
             String bestIndex = null;
             int bestMatchLen = 2; // ignore shorter matches
             AtomicBoolean dummy = new AtomicBoolean();
-            for (int i = 0; dictFilename == null && i < dics.size(); ++i)
-            {
+            for (int i = 0; dictFilename == null && i < dics.size(); ++i) {
                 try {
                     Log.d(LOG, "Checking dictionary " + dics.get(i).uncompressedFilename);
                     final File dictfile = application.getPath(dics.get(i).uncompressedFilename);
@@ -359,8 +349,7 @@ public class DictionaryActivity extends ActionBarActivity {
                     for (int j = 0; j < dic.indices.size(); ++j) {
                         Index idx = dic.indices.get(j);
                         Log.d(LOG, "Checking index " + idx.shortName);
-                        if (idx.findExact(search) != null)
-                        {
+                        if (idx.findExact(search) != null) {
                             Log.d(LOG, "Found exact match");
                             dictFilename = dictfile.toString();
                             intent.putExtra(C.INDEX_SHORT_NAME, idx.shortName);
@@ -368,8 +357,7 @@ public class DictionaryActivity extends ActionBarActivity {
                         }
                         int matchLen = getMatchLen(search, idx.findInsertionPoint(search, dummy));
                         Log.d(LOG, "Found partial match length " + matchLen);
-                        if (matchLen > bestMatchLen)
-                        {
+                        if (matchLen > bestMatchLen) {
                             bestFname = dictfile.toString();
                             bestIndex = idx.shortName;
                             bestMatchLen = matchLen;
@@ -377,15 +365,13 @@ public class DictionaryActivity extends ActionBarActivity {
                     }
                 } catch (Exception e) {}
             }
-            if (dictFilename == null && bestFname != null)
-            {
+            if (dictFilename == null && bestFname != null) {
                 dictFilename = bestFname;
                 intent.putExtra(C.INDEX_SHORT_NAME, bestIndex);
             }
         }
 
-        if (dictFilename == null)
-        {
+        if (dictFilename == null) {
             Toast.makeText(this, getString(R.string.no_dict_file), Toast.LENGTH_LONG).show();
             startActivity(DictionaryManagerActivity.getLaunchIntent(getApplicationContext()));
             finish();
@@ -419,7 +405,7 @@ public class DictionaryActivity extends ActionBarActivity {
                 dictRaf = null;
             }
             Toast.makeText(this, getString(R.string.invalidDictionary, "", e.getMessage()),
-                    Toast.LENGTH_LONG).show();
+                           Toast.LENGTH_LONG).show();
             startActivity(DictionaryManagerActivity.getLaunchIntent(getApplicationContext()));
             finish();
             return;
@@ -437,27 +423,36 @@ public class DictionaryActivity extends ActionBarActivity {
         }
         Log.d(LOG, "Loading index " + indexIndex);
         index = dictionary.indices.get(indexIndex);
+        getListView().setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
         getListView().setEmptyView(findViewById(android.R.id.empty));
+        getListView().setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int row, long id) {
+                onListItemClick(getListView(), view, row, id);
+            }
+        });
+
         setListAdapter(new IndexAdapter(index));
 
+        // Pre-load the Transliterator (will spawn its own thread)
+        TransliteratorManager.init(new TransliteratorManager.Callback() {
+            @Override
+            public void onTransliteratorReady() {
+                uiHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        onSearchTextChange(searchView.getQuery().toString());
+                    }
+                });
+            }
+        }, DictionaryApplication.threadBackground);
+
         // Pre-load the collators.
         new Thread(new Runnable() {
             public void run() {
-                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE);
                 final long startMillis = System.currentTimeMillis();
                 try {
-                    TransliteratorManager.init(new TransliteratorManager.Callback() {
-                        @Override
-                        public void onTransliteratorReady() {
-                            uiHandler.post(new Runnable() {
-                                @Override
-                                public void run() {
-                                    onSearchTextChange(searchView.getQuery().toString());
-                                }
-                            });
-                        }
-                    });
-
                     for (final Index index : dictionary.indices) {
                         final String searchToken = index.sortedIndexEntries.get(0).token;
                         final IndexEntry entry = index.findExact(searchToken);
@@ -468,7 +463,7 @@ public class DictionaryActivity extends ActionBarActivity {
                     indexPrepFinished = true;
                 } catch (Exception e) {
                     Log.w(LOG,
-                            "Exception while prepping.  This can happen if dictionary is closed while search is happening.");
+                          "Exception while prepping.  This can happen if dictionary is closed while search is happening.");
                 }
                 Log.d(LOG, "Prepping indices took:" + (System.currentTimeMillis() - startMillis));
             }
@@ -477,11 +472,11 @@ public class DictionaryActivity extends ActionBarActivity {
         String fontName = prefs.getString(getString(R.string.fontKey), "FreeSerif.otf.jpg");
         if ("SYSTEM".equals(fontName)) {
             typeface = Typeface.DEFAULT;
-       } else if ("SERIF".equals(fontName)) {
+        } else if ("SERIF".equals(fontName)) {
             typeface = Typeface.SERIF;
-       } else if ("SANS_SERIF".equals(fontName)) {
+        } else if ("SANS_SERIF".equals(fontName)) {
             typeface = Typeface.SANS_SERIF;
-       } else if ("MONOSPACE".equals(fontName)) {
+        } else if ("MONOSPACE".equals(fontName)) {
             typeface = Typeface.MONOSPACE;
         } else {
             if ("FreeSerif.ttf.jpg".equals(fontName)) {
@@ -492,7 +487,7 @@ public class DictionaryActivity extends ActionBarActivity {
             } catch (Exception e) {
                 Log.w(LOG, "Exception trying to use typeface, using default.", e);
                 Toast.makeText(this, getString(R.string.fontFailure, e.getLocalizedMessage()),
-                        Toast.LENGTH_LONG).show();
+                               Toast.LENGTH_LONG).show();
             }
         }
         if (typeface == null) {
@@ -512,20 +507,14 @@ public class DictionaryActivity extends ActionBarActivity {
         // Cache some prefs.
         wordList = application.getWordListFile();
         saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey),
-                false);
+                                false);
         clickOpensContextMenu = prefs.getBoolean(getString(R.string.clickOpensContextMenuKey),
-                false);
+                                !getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN));
         Log.d(LOG, "wordList=" + wordList + ", saveOnlyFirstSubentry=" + saveOnlyFirstSubentry);
 
         onCreateSetupActionBarAndSearchView();
 
         View floatSwapButton = findViewById(R.id.floatSwapButton);
-        floatSwapButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View arg0) {
-                onLanguageButtonClick();
-            }
-        });
         floatSwapButton.setOnLongClickListener(new OnLongClickListener() {
             @Override
             public boolean onLongClick(View v) {
@@ -534,21 +523,6 @@ public class DictionaryActivity extends ActionBarActivity {
             }
         });
 
-        final FloatingActionButton floatSearchButton = (FloatingActionButton)findViewById(R.id.floatSearchButton);
-        floatSearchButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View arg0) {
-                if (!searchView.hasFocus()) {
-                    searchView.requestFocus();
-                }
-                if (searchView.getQuery().toString().length() > 0) {
-                    searchView.setQuery("", false);
-                }
-                showKeyboard();
-                searchView.setIconified(false);
-            }
-        });
-
         // Set the search text from the intent, then the saved state.
         String text = getIntent().getStringExtra(C.SEARCH_TOKEN);
         if (savedInstanceState != null) {
@@ -578,17 +552,11 @@ public class DictionaryActivity extends ActionBarActivity {
         final LinearLayout customSearchView = new LinearLayout(getSupportActionBar().getThemedContext());
 
         final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
         customSearchView.setLayoutParams(layoutParams);
 
-        listView.setOnItemClickListener(new OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int row, long id) {
-                onListItemClick(getListView(), view, row, id);
-            }
-        });
-
         languageButton = new ImageButton(customSearchView.getContext());
+        languageButton.setId(R.id.languageButton);
         languageButton.setScaleType(ScaleType.FIT_CENTER);
         languageButton.setOnClickListener(new OnClickListener() {
             @Override
@@ -601,6 +569,7 @@ public class DictionaryActivity extends ActionBarActivity {
         customSearchView.addView(languageButton, lpb);
 
         searchView = new SearchView(getSupportActionBar().getThemedContext());
+        searchView.setId(R.id.searchView);
 
         // Get rid of search icon, it takes up too much space.
         // There is still text saying "search" in the search field.
@@ -611,11 +580,11 @@ public class DictionaryActivity extends ActionBarActivity {
         searchView.setSubmitButtonEnabled(false);
         searchView.setInputType(InputType.TYPE_CLASS_TEXT);
         searchView.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);
+            EditorInfo.IME_ACTION_DONE |
+            EditorInfo.IME_FLAG_NO_EXTRACT_UI |
+            // EditorInfo.IME_FLAG_NO_FULLSCREEN | // Requires API
+            // 11
+            EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
         onQueryTextListener = new OnQueryTextListener() {
             @Override
             public boolean onQueryTextSubmit(String query) {
@@ -640,9 +609,13 @@ public class DictionaryActivity extends ActionBarActivity {
         actionBar.setCustomView(customSearchView);
         actionBar.setDisplayShowCustomEnabled(true);
 
-       // Avoid wasting space on large left inset
+        // Avoid wasting space on large left inset
         Toolbar tb = (Toolbar)customSearchView.getParent();
         tb.setContentInsetsRelative(0, 0);
+
+        getListView().setNextFocusLeftId(R.id.searchView);
+        findViewById(R.id.floatSwapButton).setNextFocusRightId(R.id.languageButton);
+        languageButton.setNextFocusLeftId(R.id.floatSwapButton);
     }
 
     @Override
@@ -676,7 +649,7 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     private static void setDictionaryPrefs(final Context context, final File dictFile,
-            final String indexShortName, final String searchToken) {
+                                           final String indexShortName, final String searchToken) {
         final SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(
                 context).edit();
         prefs.putString(C.DICT_FILE, dictFile.getPath());
@@ -701,6 +674,9 @@ public class DictionaryActivity extends ActionBarActivity {
             currentSearchOperation = null;
             searchOperation.interrupted.set(true);
         }
+        searchExecutor.shutdownNow();
+        textToSpeech.shutdown();
+        textToSpeech = null;
 
         try {
             Log.d(LOG, "Closing RAF.");
@@ -744,10 +720,9 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     void updateLangButton() {
-        final LanguageResources languageResources =
-                DictionaryApplication.isoCodeToResources.get(index.shortName);
-        if (languageResources != null && languageResources.flagId != 0) {
-            languageButton.setImageResource(languageResources.flagId);
+        final int flagId = IsoUtils.INSTANCE.getFlagIdForIsoCode(index.shortName);
+        if (flagId != 0) {
+            languageButton.setImageResource(flagId);
         } else {
             if (indexIndex % 2 == 0) {
                 languageButton.setImageResource(android.R.drawable.ic_media_next);
@@ -766,17 +741,28 @@ public class DictionaryActivity extends ActionBarActivity {
         final Locale locale = new Locale(dictionary.indices.get(i).sortLanguage.getIsoCode());
         Log.d(LOG, "Setting TTS locale to: " + locale);
         try {
-        final int ttsResult = textToSpeech.setLanguage(locale);
-        if (ttsResult != TextToSpeech.LANG_AVAILABLE &&
-                ttsResult != TextToSpeech.LANG_COUNTRY_AVAILABLE) {
-            Log.e(LOG, "TTS not available in this language: ttsResult=" + ttsResult);
-        }
+            final int ttsResult = textToSpeech.setLanguage(locale);
+            if (ttsResult != TextToSpeech.LANG_AVAILABLE &&
+                    ttsResult != TextToSpeech.LANG_COUNTRY_AVAILABLE) {
+                Log.e(LOG, "TTS not available in this language: ttsResult=" + ttsResult);
+            }
         } catch (Exception e) {
             Toast.makeText(this, getString(R.string.TTSbroken), Toast.LENGTH_LONG).show();
         }
     }
 
-    void onLanguageButtonClick() {
+    public void onSearchButtonClick(View dummy) {
+        if (!searchView.hasFocus()) {
+            searchView.requestFocus();
+        }
+        if (searchView.getQuery().toString().length() > 0) {
+            searchView.setQuery("", false);
+        }
+        showKeyboard();
+        searchView.setIconified(false);
+    }
+
+    public void onLanguageButtonClick(View dummy) {
         if (dictionary.indices.size() == 1) {
             // No need to work to switch indices.
             return;
@@ -786,7 +772,7 @@ public class DictionaryActivity extends ActionBarActivity {
             currentSearchOperation = null;
         }
         setIndexAndSearchText((indexIndex + 1) % dictionary.indices.size(),
-                searchView.getQuery().toString(), false);
+                              searchView.getQuery().toString(), false);
     }
 
     void onLanguageButtonLongClick(final Context context) {
@@ -801,7 +787,7 @@ public class DictionaryActivity extends ActionBarActivity {
         final String name = getString(R.string.dictionaryManager);
         button.setText(name);
         final IntentLauncher intentLauncher = new IntentLauncher(listView.getContext(),
-                DictionaryManagerActivity.getLaunchIntent(getApplicationContext())) {
+        DictionaryManagerActivity.getLaunchIntent(getApplicationContext())) {
             @Override
             protected void onGo() {
                 dialog.dismiss();
@@ -811,6 +797,7 @@ public class DictionaryActivity extends ActionBarActivity {
         button.setOnClickListener(intentLauncher);
         listView.addHeaderView(button);
 
+        listView.setItemsCanFocus(true);
         listView.setAdapter(new BaseAdapter() {
             @Override
             public View getView(int position, View convertView, ViewGroup parent) {
@@ -820,12 +807,12 @@ public class DictionaryActivity extends ActionBarActivity {
 
                 for (int i = 0; i < dictionaryInfo.indexInfos.size(); ++i) {
                     final IndexInfo indexInfo = dictionaryInfo.indexInfos.get(i);
-                    final View button = application.createButton(parent.getContext(),
-                            dictionaryInfo, indexInfo);
+                    final View button = IsoUtils.INSTANCE.createButton(parent.getContext(),
+                                        dictionaryInfo, indexInfo, application.languageButtonPixels);
                     final IntentLauncher intentLauncher = new IntentLauncher(parent.getContext(),
                             getLaunchIntent(getApplicationContext(),
-                                    application.getPath(dictionaryInfo.uncompressedFilename),
-                                    indexInfo.shortName, searchView.getQuery().toString())) {
+                                            application.getPath(dictionaryInfo.uncompressedFilename),
+                    indexInfo.shortName, searchView.getQuery().toString())) {
                         @Override
                         protected void onGo() {
                             dialog.dismiss();
@@ -834,7 +821,7 @@ public class DictionaryActivity extends ActionBarActivity {
                     };
                     button.setOnClickListener(intentLauncher);
                     if (i == indexIndex && dictFile != null &&
-                        dictFile.getName().equals(dictionaryInfo.uncompressedFilename)) {
+                            dictFile.getName().equals(dictionaryInfo.uncompressedFilename)) {
                         button.setPressed(true);
                     }
                     result.addView(button);
@@ -842,10 +829,10 @@ public class DictionaryActivity extends ActionBarActivity {
 
                 final TextView nameView = new TextView(parent.getContext());
                 final String name = application
-                        .getDictionaryName(dictionaryInfo.uncompressedFilename);
+                                    .getDictionaryName(dictionaryInfo.uncompressedFilename);
                 nameView.setText(name);
                 final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
-                        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                 layoutParams.width = 0;
                 layoutParams.weight = 1.0f;
                 nameView.setLayoutParams(layoutParams);
@@ -918,7 +905,7 @@ public class DictionaryActivity extends ActionBarActivity {
                 .getBoolean(getString(R.string.showPrevNextButtonsKey), true)) {
             // Next word.
             nextWordMenuItem = menu.add(getString(R.string.nextWord))
-                    .setIcon(R.drawable.arrow_down_float);
+                               .setIcon(R.drawable.arrow_down_float);
             MenuItemCompat.setShowAsAction(nextWordMenuItem, MenuItem.SHOW_AS_ACTION_IF_ROOM);
             nextWordMenuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
                 @Override
@@ -930,7 +917,7 @@ public class DictionaryActivity extends ActionBarActivity {
 
             // Previous word.
             previousWordMenuItem = menu.add(getString(R.string.previousWord))
-                    .setIcon(R.drawable.arrow_up_float);
+                                   .setIcon(R.drawable.arrow_up_float);
             MenuItemCompat.setShowAsAction(previousWordMenuItem, MenuItem.SHOW_AS_ACTION_IF_ROOM);
             previousWordMenuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
                 @Override
@@ -983,27 +970,27 @@ public class DictionaryActivity extends ActionBarActivity {
                     if (dictionaryInfo != null) {
                         builder.append(dictionaryInfo.dictInfo).append("\n\n");
                         builder.append(getString(R.string.dictionaryPath, dictFile.getPath()))
-                                .append("\n");
+                        .append("\n");
                         builder.append(
-                                getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes))
-                                .append("\n");
+                            getString(R.string.dictionarySize, dictionaryInfo.uncompressedBytes))
+                        .append("\n");
                         builder.append(
-                                getString(R.string.dictionaryCreationTime,
-                                        dictionaryInfo.creationMillis)).append("\n");
+                            getString(R.string.dictionaryCreationTime,
+                                      dictionaryInfo.creationMillis)).append("\n");
                         for (final IndexInfo indexInfo : dictionaryInfo.indexInfos) {
                             builder.append("\n");
                             builder.append(getString(R.string.indexName, indexInfo.shortName))
-                                    .append("\n");
+                            .append("\n");
                             builder.append(
-                                    getString(R.string.mainTokenCount, indexInfo.mainTokenCount))
-                                    .append("\n");
+                                getString(R.string.mainTokenCount, indexInfo.mainTokenCount))
+                            .append("\n");
                         }
                         builder.append("\n");
                         builder.append(getString(R.string.sources)).append("\n");
                         for (final EntrySource source : dictionary.sources) {
                             builder.append(
-                                    getString(R.string.sourceInfo, source.getName(),
-                                            source.getNumEntries())).append("\n");
+                                getString(R.string.sourceInfo, source.getName(),
+                                          source.getNumEntries())).append("\n");
                         }
                     }
                     textView.setText(builder.toString());
@@ -1027,18 +1014,31 @@ public class DictionaryActivity extends ActionBarActivity {
 
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-        AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;
+        final AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) menuInfo;
         final RowBase row = (RowBase) getListAdapter().getItem(adapterContextMenuInfo.position);
 
+        if (clickOpensContextMenu && (row instanceof HtmlEntry.Row ||
+            (row instanceof TokenRow && ((TokenRow)row).getIndexEntry().htmlEntries.size() > 0))) {
+            final List<HtmlEntry> html = row instanceof TokenRow ? ((TokenRow)row).getIndexEntry().htmlEntries : Collections.singletonList(((HtmlEntry.Row)row).getEntry());
+            final String highlight = row instanceof HtmlEntry.Row ? ((HtmlEntry.Row)row).getTokenRow(true).getToken() : null;
+            final MenuItem open = menu.add("Open");
+            open.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+                public boolean onMenuItemClick(MenuItem item) {
+                    showHtml(html, highlight);
+                    return false;
+                }
+            });
+        }
+
         final android.view.MenuItem addToWordlist = menu.add(getString(R.string.addToWordList,
                 wordList.getName()));
         addToWordlist
-                .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
-                    public boolean onMenuItemClick(android.view.MenuItem item) {
-                        onAppendToWordList(row);
-                        return false;
-                    }
-                });
+        .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
+            public boolean onMenuItemClick(android.view.MenuItem item) {
+                onAppendToWordList(row);
+                return false;
+            }
+        });
 
         final android.view.MenuItem share = menu.add("Share");
         share.setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
@@ -1046,9 +1046,9 @@ public class DictionaryActivity extends ActionBarActivity {
                 Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
                 shareIntent.setType("text/plain");
                 shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, row.getTokenRow(true)
-                        .getToken());
+                                     .getToken());
                 shareIntent.putExtra(android.content.Intent.EXTRA_TEXT,
-                        row.getRawText(saveOnlyFirstSubentry));
+                                     row.getRawText(saveOnlyFirstSubentry));
                 startActivity(shareIntent);
                 return false;
             }
@@ -1065,15 +1065,15 @@ public class DictionaryActivity extends ActionBarActivity {
         if (selectedSpannableText != null) {
             final String selectedText = selectedSpannableText;
             final android.view.MenuItem searchForSelection = menu.add(getString(
-                    R.string.searchForSelection,
-                    selectedSpannableText));
+                        R.string.searchForSelection,
+                        selectedSpannableText));
             searchForSelection
-                    .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
-                        public boolean onMenuItemClick(android.view.MenuItem item) {
-                            jumpToTextFromHyperLink(selectedText, selectedSpannableIndex);
-                            return false;
-                        }
-                    });
+            .setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
+                public boolean onMenuItemClick(android.view.MenuItem item) {
+                    jumpToTextFromHyperLink(selectedText, selectedSpannableIndex);
+                    return false;
+                }
+            });
             // Rats, this won't be shown:
             //searchForSelection.setIcon(R.drawable.abs__ic_search);
         }
@@ -1086,7 +1086,38 @@ public class DictionaryActivity extends ActionBarActivity {
                 @Override
                 public boolean onMenuItemClick(android.view.MenuItem item) {
                     textToSpeech.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH,
-                            new HashMap<String, String>());
+                                       new HashMap<String, String>());
+                    return false;
+                }
+            });
+        }
+        if (row instanceof PairEntry.Row && ttsReady) {
+            final List<Pair> pairs = ((PairEntry.Row)row).getEntry().pairs;
+            final MenuItem speakLeft = menu.add(R.string.speak_left);
+            speakLeft.setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
+                @Override
+                public boolean onMenuItemClick(android.view.MenuItem item) {
+                    int idx = index.swapPairEntries ? 1 : 0;
+                    updateTTSLanguage(idx);
+                    String text = "";
+                    for (Pair p : pairs) text += p.get(idx);
+                    text = text.replaceAll("\\{[^{}]*\\}", "").replace("{", "").replace("}", "");
+                    textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH,
+                                       new HashMap<String, String>());
+                    return false;
+                }
+            });
+            final MenuItem speakRight = menu.add(R.string.speak_right);
+            speakRight.setOnMenuItemClickListener(new android.view.MenuItem.OnMenuItemClickListener() {
+                @Override
+                public boolean onMenuItemClick(android.view.MenuItem item) {
+                    int idx = index.swapPairEntries ? 0 : 1;
+                    updateTTSLanguage(idx);
+                    String text = "";
+                    for (Pair p : pairs) text += p.get(idx);
+                    text = text.replaceAll("\\{[^{}]*\\}", "").replace("{", "").replace("}", "");
+                    textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH,
+                                       new HashMap<String, String>());
                     return false;
                 }
             });
@@ -1094,7 +1125,7 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     private void jumpToTextFromHyperLink(
-            final String selectedText, final int defaultIndexToUse) {
+        final String selectedText, final int defaultIndexToUse) {
         int indexToUse = -1;
         int numFound = 0;
         for (int i = 0; i < dictionary.indices.size(); ++i) {
@@ -1104,7 +1135,7 @@ public class DictionaryActivity extends ActionBarActivity {
                 final IndexEntry indexEntry = index.findExact(selectedText);
                 if (indexEntry != null) {
                     final TokenRow tokenRow = index.rows.get(indexEntry.startRow)
-                            .getTokenRow(false);
+                                              .getTokenRow(false);
                     if (tokenRow != null && tokenRow.hasMainEntry) {
                         indexToUse = i;
                         ++numFound;
@@ -1143,10 +1174,15 @@ public class DictionaryActivity extends ActionBarActivity {
         // searchView.selectAll();
     }
 
-    protected void onListItemClick(ListView l, View v, int row, long id) {
+    protected void onListItemClick(ListView l, View v, int rowIdx, long id) {
         defocusSearchText();
         if (clickOpensContextMenu && dictRaf != null) {
             openContextMenu(v);
+        } else {
+            final RowBase row = (RowBase)getListAdapter().getItem(rowIdx);
+            if (!(row instanceof PairEntry.Row)) {
+                v.performClick();
+            }
         }
     }
 
@@ -1169,8 +1205,8 @@ public class DictionaryActivity extends ActionBarActivity {
         } catch (Exception e) {
             Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
             Toast.makeText(this,
-                    getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()),
-                    Toast.LENGTH_LONG).show();
+                           getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()),
+                           Toast.LENGTH_LONG).show();
         }
         return;
     }
@@ -1208,7 +1244,7 @@ public class DictionaryActivity extends ActionBarActivity {
             View focus = getCurrentFocus();
             if (focus != null) {
                 inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
-                        InputMethodManager.HIDE_NOT_ALWAYS);
+                                                     InputMethodManager.HIDE_NOT_ALWAYS);
             }
             return true;
         }
@@ -1252,7 +1288,7 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     private void setSearchText(final String text, final boolean triggerSearch) {
-       setSearchText(text, triggerSearch, true);
+        setSearchText(text, triggerSearch, true);
     }
 
     // private long cursorDelayMillis = 100;
@@ -1359,12 +1395,12 @@ public class DictionaryActivity extends ActionBarActivity {
                 } else {
                     searchTokens = Arrays.asList(searchTokenArray);
                     multiWordSearchResult = index.multiWordSearch(searchText, searchTokens,
-                            interrupted);
+                                            interrupted);
                 }
                 Log.d(LOG,
-                        "searchText=" + searchText + ", searchDuration="
-                                + (System.currentTimeMillis() - searchStartMillis)
-                                + ", interrupted=" + interrupted.get());
+                      "searchText=" + searchText + ", searchDuration="
+                      + (System.currentTimeMillis() - searchStartMillis)
+                      + ", interrupted=" + interrupted.get());
                 if (!interrupted.get()) {
                     uiHandler.post(new Runnable() {
                         @Override
@@ -1390,11 +1426,21 @@ public class DictionaryActivity extends ActionBarActivity {
     // IndexAdapter
     // --------------------------------------------------------------------------
 
+    private void showHtml(final List<HtmlEntry> htmlEntries, final String htmlTextToHighlight) {
+        String html = HtmlEntry.htmlBody(htmlEntries, index.shortName);
+        // Log.d(LOG, "html=" + html);
+        startActivityForResult(
+            HtmlDisplayActivity.getHtmlIntent(getApplicationContext(), String.format(
+                    "<html><head><meta name=\"viewport\" content=\"width=device-width\"></head><body>%s</body></html>", html),
+                                              htmlTextToHighlight, false),
+            0);
+    }
+
     static ViewGroup.LayoutParams WEIGHT_1 = new LinearLayout.LayoutParams(
-            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
+        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
 
     static ViewGroup.LayoutParams WEIGHT_0 = new LinearLayout.LayoutParams(
-            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 0.0f);
+        ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 0.0f);
 
     final class IndexAdapter extends BaseAdapter {
 
@@ -1428,7 +1474,12 @@ public class DictionaryActivity extends ActionBarActivity {
 
         private void getMetrics() {
             // Get the screen's density scale
-            final float scale = getResources().getDisplayMetrics().density;
+            // The previous method getResources().getDisplayMetrics()
+            // used to occasionally trigger a null pointer exception,
+            // so try this instead.
+            DisplayMetrics dm = new DisplayMetrics();
+            getWindowManager().getDefaultDisplay().getMetrics(dm);
+            final float scale = dm.density;
             // Convert the dps to pixels, based on density scale
             mPaddingDefault = (int) (PADDING_DEFAULT_DP * scale + 0.5f);
             mPaddingLarge = (int) (PADDING_LARGE_DP * scale + 0.5f);
@@ -1471,7 +1522,7 @@ public class DictionaryActivity extends ActionBarActivity {
         }
 
         private TableLayout getView(final int position, PairEntry.Row row, ViewGroup parent,
-                final TableLayout result) {
+                                    final TableLayout result) {
             final PairEntry entry = row.getEntry();
             final int rowCount = entry.pairs.size();
 
@@ -1484,6 +1535,10 @@ public class DictionaryActivity extends ActionBarActivity {
 
                 final TextView col1 = new TextView(tableRow.getContext());
                 final TextView col2 = new TextView(tableRow.getContext());
+                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+                    col1.setTextIsSelectable(true);
+                    col2.setTextIsSelectable(true);
+                }
 
                 // Set the columns in the table.
                 if (r > 0) {
@@ -1515,13 +1570,13 @@ public class DictionaryActivity extends ActionBarActivity {
 
                 // Bold the token instances in col1.
                 final Set<String> toBold = toHighlight != null ? this.toHighlight : Collections
-                        .singleton(row.getTokenRow(true).getToken());
+                                           .singleton(row.getTokenRow(true).getToken());
                 final Spannable col1Spannable = (Spannable) col1.getText();
                 for (final String token : toBold) {
                     int startPos = 0;
                     while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
                         col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos, startPos
-                                + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+                                              + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
                         startPos += token.length();
                     }
                 }
@@ -1550,7 +1605,7 @@ public class DictionaryActivity extends ActionBarActivity {
             // http://groups.google.com/group/android-developers/browse_thread/thread/3d96af1530a7d62a?pli=1
             result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
             result.setClickable(true);
-            result.setFocusable(true);
+            result.setFocusable(false);
             result.setLongClickable(true);
 //            result.setBackgroundResource(android.R.drawable.menuitem_background);
 
@@ -1573,7 +1628,7 @@ public class DictionaryActivity extends ActionBarActivity {
 
             final TableRow tableRow = new TableRow(result.getContext());
             tableRow.setBackgroundResource(hasMainEntry ? theme.tokenRowMainBg
-                    : theme.tokenRowOtherBg);
+                                           : theme.tokenRowOtherBg);
             if (isTokenRow) {
                 tableRow.setPadding(mPaddingDefault, mPaddingDefault, mPaddingDefault, 0);
             } else {
@@ -1617,32 +1672,33 @@ public class DictionaryActivity extends ActionBarActivity {
                 textView.setOnClickListener(new OnClickListener() {
                     @Override
                     public void onClick(View v) {
-                        String html = HtmlEntry.htmlBody(htmlEntries, index.shortName);
-                        // Log.d(LOG, "html=" + html);
-                        startActivityForResult(
-                                HtmlDisplayActivity.getHtmlIntent(getApplicationContext(), String.format(
-                                        "<html><head></head><body>%s</body></html>", html),
-                                        htmlTextToHighlight, false),
-                                0);
+                        showHtml(htmlEntries, htmlTextToHighlight);
+                    }
+                });
+                result.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        textView.performClick();
                     }
                 });
             }
+            result.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
             return result;
         }
 
         private TableLayout getView(TokenRow row, ViewGroup parent, final TableLayout result) {
             final IndexEntry indexEntry = row.getIndexEntry();
             return getPossibleLinkToHtmlEntryView(true, indexEntry.token, row.hasMainEntry,
-                    indexEntry.htmlEntries, null, parent, result);
+                                                  indexEntry.htmlEntries, null, parent, result);
         }
 
         private TableLayout getView(HtmlEntry.Row row, ViewGroup parent, final TableLayout result) {
             final HtmlEntry htmlEntry = row.getEntry();
             final TokenRow tokenRow = row.getTokenRow(true);
             return getPossibleLinkToHtmlEntryView(false,
-                    getString(R.string.seeAlso, htmlEntry.title, htmlEntry.entrySource.getName()),
-                    false, Collections.singletonList(htmlEntry), tokenRow.getToken(), parent,
-                    result);
+                                                  getString(R.string.seeAlso, htmlEntry.title, htmlEntry.entrySource.getName()),
+                                                  false, Collections.singletonList(htmlEntry), tokenRow.getToken(), parent,
+                                                  result);
         }
 
     }
@@ -1650,15 +1706,15 @@ public class DictionaryActivity extends ActionBarActivity {
     static final Pattern CHAR_DASH = Pattern.compile("['\\p{L}\\p{M}\\p{N}]+");
 
     private void createTokenLinkSpans(final TextView textView, final Spannable spannable,
-            final String text) {
+                                      final String text) {
         // Saw from the source code that LinkMovementMethod sets the selection!
         // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.1_r1/android/text/method/LinkMovementMethod.java#LinkMovementMethod
         textView.setMovementMethod(LinkMovementMethod.getInstance());
         final Matcher matcher = CHAR_DASH.matcher(text);
         while (matcher.find()) {
             spannable.setSpan(new NonLinkClickableSpan(textColorFg), matcher.start(),
-                    matcher.end(),
-                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+                              matcher.end(),
+                              Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
         }
     }
 
@@ -1694,10 +1750,10 @@ public class DictionaryActivity extends ActionBarActivity {
     }
 
     final TextViewLongClickListener textViewLongClickListenerIndex0 = new TextViewLongClickListener(
-            0);
+        0);
 
     final TextViewLongClickListener textViewLongClickListenerIndex1 = new TextViewLongClickListener(
-            1);
+        1);
 
     // --------------------------------------------------------------------------
     // SearchText