/* * Copyright (C) 2010-2015 FBReader.ORG Limited <contact@fbreader.org> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ package org.geometerplus.android.fbreader.dict; import java.util.*; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import android.app.Activity; import android.content.*; import android.net.Uri; import android.util.DisplayMetrics; import org.geometerplus.zlibrary.core.filesystem.ZLFile; import org.geometerplus.zlibrary.core.language.Language; import org.geometerplus.zlibrary.core.options.ZLEnumOption; import org.geometerplus.zlibrary.core.options.ZLStringOption; import org.geometerplus.zlibrary.core.util.XmlUtil; import org.geometerplus.fbreader.fbreader.DurationEnum; import org.geometerplus.android.fbreader.FBReaderMainActivity; import org.geometerplus.android.util.PackageUtil; public abstract class DictionaryUtil { public static final ZLEnumOption<DurationEnum> TranslationToastDurationOption = new ZLEnumOption<DurationEnum>("Dictionary", "TranslationToastDuration", DurationEnum.duration40); public static final ZLEnumOption<DurationEnum> ErrorToastDurationOption = new ZLEnumOption<DurationEnum>("Dictionary", "ErrorToastDuration", DurationEnum.duration5); private static int FLAG_INSTALLED_ONLY = 1; static int FLAG_SHOW_AS_DICTIONARY = 2; private static int FLAG_SHOW_AS_TRANSLATOR = 4; private static ZLStringOption ourSingleWordTranslatorOption; private static ZLStringOption ourMultiWordTranslatorOption; // TODO: use StringListOption instead public static final ZLStringOption TargetLanguageOption = new ZLStringOption("Dictionary", "TargetLanguage", Language.ANY_CODE); // Map: dictionary info -> mode if package is not installed private static Map<PackageInfo,Integer> ourInfos = Collections.synchronizedMap(new LinkedHashMap<PackageInfo,Integer>()); public static abstract class PackageInfo extends HashMap<String,String> { public final boolean SupportsTargetLanguageSetting; PackageInfo(String id, String title) { this(id, title, false); } PackageInfo(String id, String title, boolean supportsTargetLanguageSetting) { put("id", id); put("title", title != null ? title : id); SupportsTargetLanguageSetting = supportsTargetLanguageSetting; } public final String getId() { return get("id"); } public final String getTitle() { return get("title"); } final Intent getActionIntent(String text) { final Intent intent = new Intent(get("action")); final String packageName = get("package"); if (packageName != null) { final String className = get("class"); if (className != null) { intent.setComponent(new ComponentName( packageName, className.startsWith(".") ? packageName + className : className )); } } final String category = get("category"); if (category != null) { intent.addCategory(category); } final String key = get("dataKey"); if (key != null) { return intent.putExtra(key, text); } else { return intent.setData(Uri.parse(text)); } } void onActivityResult(FBReaderMainActivity fbreader, int resultCode, final Intent data) { // does nothing; implement in subclasses } abstract void open(String text, Runnable outliner, FBReaderMainActivity fbreader, PopupFrameMetric frameMetrics); } private static class PlainPackageInfo extends PackageInfo { PlainPackageInfo(String id, String title) { super(id, title); } @Override void open(String text, Runnable outliner, FBReaderMainActivity fbreader, PopupFrameMetric frameMetrics) { final Intent intent = getActionIntent(text); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); InternalUtil.startDictionaryActivity(fbreader, intent, this); } } private static class InfoReader extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (!"dictionary".equals(localName)) { return; } final String id = attributes.getValue("id"); final String title = attributes.getValue("title"); final String role = attributes.getValue("role"); int flags; if ("dictionary".equals(role)) { flags = FLAG_SHOW_AS_DICTIONARY; } else if ("translator".equals(role)) { flags = FLAG_SHOW_AS_TRANSLATOR; } else { flags = FLAG_SHOW_AS_DICTIONARY | FLAG_SHOW_AS_TRANSLATOR; } if (!"always".equals(attributes.getValue("list"))) { flags |= FLAG_INSTALLED_ONLY; } final PackageInfo info; if ("dictan".equals(id)) { info = new Dictan(id, title); } else if ("ABBYY Lingvo".equals(id)) { info = new Lingvo(id, title); } else if ("ColorDict".equals(id)) { info = new ColorDict(id, title); } else { info = new PlainPackageInfo(id, title); } for (int i = attributes.getLength() - 1; i >= 0; --i) { info.put(attributes.getLocalName(i), attributes.getValue(i)); } ourInfos.put(info, flags); } } private static class BitKnightsInfoReader extends DefaultHandler { private final Context myContext; private int myCounter; BitKnightsInfoReader(Context context) { myContext = context; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (!"dictionary".equals(localName)) { return; } final PackageInfo info = new PlainPackageInfo( "BK" + myCounter ++, attributes.getValue("title") ); for (int i = attributes.getLength() - 1; i >= 0; --i) { info.put(attributes.getLocalName(i), attributes.getValue(i)); } info.put("class", "com.bitknights.dict.ShareTranslateActivity"); info.put("action", Intent.ACTION_VIEW); // TODO: other attributes if (PackageUtil.canBeStarted(myContext, info.getActionIntent("test"), false)) { ourInfos.put(info, FLAG_SHOW_AS_DICTIONARY | FLAG_INSTALLED_ONLY); } } } private static final class Initializer implements Runnable { private final Activity myActivity; private final Runnable myPostAction; public Initializer(Activity activity, Runnable postAction) { myActivity = activity; myPostAction = postAction; } public void run() { synchronized (ourInfos) { if (!ourInfos.isEmpty()) { if (myPostAction != null) { myPostAction.run(); } return; } XmlUtil.parseQuietly( ZLFile.createFileByPath("dictionaries/main.xml"), new InfoReader() ); XmlUtil.parseQuietly( ZLFile.createFileByPath("dictionaries/bitknights.xml"), new BitKnightsInfoReader(myActivity) ); myActivity.runOnUiThread(new Runnable() { public void run() { OpenDictionary.collect(myActivity, ourInfos); if (myPostAction != null) { myPostAction.run(); } } }); } } } public static void init(Activity activity, Runnable postAction) { if (ourInfos.isEmpty()) { final Thread initThread = new Thread(new Initializer(activity, postAction)); initThread.setPriority(Thread.MIN_PRIORITY); initThread.start(); } else if (postAction != null) { postAction.run(); } } public static List<PackageInfo> dictionaryInfos(Context context, boolean dictionaryNotTranslator) { final LinkedList<PackageInfo> list = new LinkedList<PackageInfo>(); final HashSet<String> installedPackages = new HashSet<String>(); final HashSet<String> notInstalledPackages = new HashSet<String>(); synchronized (ourInfos) { for (Map.Entry<PackageInfo,Integer> entry : ourInfos.entrySet()) { final PackageInfo info = entry.getKey(); final int flags = entry.getValue(); if (dictionaryNotTranslator) { if ((flags & FLAG_SHOW_AS_DICTIONARY) == 0) { continue; } } else { if ((flags & FLAG_SHOW_AS_TRANSLATOR) == 0) { continue; } } final String packageName = info.get("package"); if (((flags & FLAG_INSTALLED_ONLY) == 0) || installedPackages.contains(packageName)) { list.add(info); } else if (!notInstalledPackages.contains(packageName)) { if (PackageUtil.canBeStarted(context, info.getActionIntent("test"), false)) { list.add(info); installedPackages.add(packageName); } else { notInstalledPackages.add(packageName); } } } } return list; } private static PackageInfo firstInfo() { synchronized (ourInfos) { for (Map.Entry<PackageInfo,Integer> entry : ourInfos.entrySet()) { if ((entry.getValue() & FLAG_INSTALLED_ONLY) == 0) { return entry.getKey(); } } } throw new RuntimeException("There are no available dictionary infos"); } public static ZLStringOption singleWordTranslatorOption() { if (ourSingleWordTranslatorOption == null) { ourSingleWordTranslatorOption = new ZLStringOption("Dictionary", "Id", firstInfo().getId()); } return ourSingleWordTranslatorOption; } public static ZLStringOption multiWordTranslatorOption() { if (ourMultiWordTranslatorOption == null) { ourMultiWordTranslatorOption = new ZLStringOption("Translator", "Id", firstInfo().getId()); } return ourMultiWordTranslatorOption; } private static PackageInfo getDictionaryInfo(String id) { if (id == null) { return firstInfo(); } synchronized (ourInfos) { for (PackageInfo info : ourInfos.keySet()) { if (id.equals(info.getId())) { return info; } } } return firstInfo(); } public static PackageInfo getCurrentDictionaryInfo(boolean singleWord) { final ZLStringOption option = singleWord ? singleWordTranslatorOption() : multiWordTranslatorOption(); return getDictionaryInfo(option.getValue()); } public static class PopupFrameMetric { public final int Height; public final int Gravity; PopupFrameMetric(DisplayMetrics metrics, int selectionTop, int selectionBottom) { final int screenHeight = metrics.heightPixels; final int topSpace = selectionTop; final int bottomSpace = metrics.heightPixels - selectionBottom; final boolean showAtBottom = bottomSpace >= topSpace; final int space = (showAtBottom ? bottomSpace : topSpace) - metrics.densityDpi / 12; final int maxHeight = Math.min(metrics.densityDpi * 20 / 12, screenHeight * 2 / 3); final int minHeight = Math.min(metrics.densityDpi * 10 / 12, screenHeight * 2 / 3); Height = Math.max(minHeight, Math.min(maxHeight, space)); Gravity = showAtBottom ? android.view.Gravity.BOTTOM : android.view.Gravity.TOP; } } public static void openTextInDictionary(final FBReaderMainActivity fbreader, String text, boolean singleWord, int selectionTop, int selectionBottom, final Runnable outliner) { final String textToTranslate; if (singleWord) { int start = 0; int end = text.length(); for (; start < end && !Character.isLetterOrDigit(text.charAt(start)); ++start); for (; start < end && !Character.isLetterOrDigit(text.charAt(end - 1)); --end); if (start == end) { return; } textToTranslate = text.substring(start, end); } else { textToTranslate = text; } final DisplayMetrics metrics = new DisplayMetrics(); fbreader.getWindowManager().getDefaultDisplay().getMetrics(metrics); final PopupFrameMetric frameMetrics = new PopupFrameMetric(metrics, selectionTop, selectionBottom); final PackageInfo info = getCurrentDictionaryInfo(singleWord); fbreader.runOnUiThread(new Runnable() { public void run() { info.open(textToTranslate, outliner, fbreader, frameMetrics); } }); } public static void onActivityResult(final FBReaderMainActivity fbreader, int resultCode, final Intent data) { getDictionaryInfo("dictan").onActivityResult(fbreader, resultCode, data); } }