1 package org.coolreader.crengine; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.UnsupportedEncodingException; 7 import java.util.ArrayList; 8 9 import org.coolreader.CoolReader; 10 import org.coolreader.R; 11 12 import android.content.Context; 13 import android.util.Log; 14 15 public class HelpFileGenerator { 16 17 private static final int MANUAL_VERSION = 4; 18 19 private final Context context; 20 private final Engine engine; 21 private final String langCode; 22 private final Properties settings; 23 private final String version; HelpFileGenerator(CoolReader context, Engine engine, Properties props, String langCode)24 public HelpFileGenerator(CoolReader context, Engine engine, Properties props, String langCode) { 25 this.context = context; 26 this.engine = engine; 27 this.langCode = langCode; 28 this.settings = props; 29 this.version = context.getVersion(); 30 } 31 32 private static final String[] settingsUsedInManual = { 33 "app.tapzone.action.tap.long.1", 34 "app.tapzone.action.tap.long.2", 35 "app.tapzone.action.tap.long.3", 36 "app.tapzone.action.tap.long.4", 37 "app.tapzone.action.tap.long.5", 38 "app.tapzone.action.tap.long.6", 39 "app.tapzone.action.tap.long.7", 40 "app.tapzone.action.tap.long.8", 41 "app.tapzone.action.tap.long.9", 42 Settings.PROP_APP_DOUBLE_TAP_SELECTION, 43 Settings.PROP_APP_TAP_ZONE_ACTIONS_TAP, 44 Settings.PROP_APP_KEY_ACTIONS_PRESS, 45 Settings.PROP_APP_TRACKBALL_DISABLED, 46 Settings.PROP_APP_FLICK_BACKLIGHT_CONTROL, 47 Settings.PROP_APP_SELECTION_ACTION, 48 Settings.PROP_APP_MULTI_SELECTION_ACTION, 49 Settings.PROP_APP_FILE_BROWSER_HIDE_EMPTY_FOLDERS, 50 Settings.PROP_APP_FILE_BROWSER_SIMPLE_MODE, 51 Settings.PROP_APP_SECONDARY_TAP_ACTION_TYPE, 52 Settings.PROP_APP_GESTURE_PAGE_FLIPPING, 53 Settings.PROP_APP_HIGHLIGHT_BOOKMARKS, 54 }; 55 getSettingHash(String name)56 private int getSettingHash(String name) { 57 String value = settings.getProperty(name); 58 return value == null ? 0 : value.hashCode(); 59 } 60 61 /** 62 * Calculate hash for current values of settings which may be affect help file content generation. 63 * @return hash value 64 */ getSettingsHash()65 public int getSettingsHash() { 66 int res = MANUAL_VERSION; 67 for (String setting : settingsUsedInManual) 68 res = res * 31 + getSettingHash(setting); 69 return res; 70 } 71 getHelpFileName(File dir, String lang)72 public static File getHelpFileName(File dir, String lang) { 73 File fn = new File(dir, "cr3_manual_" + lang + ".fb2"); 74 return fn; 75 } 76 getHelpFileName(File dir)77 public File getHelpFileName(File dir) { 78 return getHelpFileName(dir, langCode); 79 } 80 generateHelpFile(File dir)81 public File generateHelpFile(File dir) { 82 String template = readTemplate(); 83 if (template == null || template.length() == 0) 84 return null; 85 String content = filterTemplate(template); 86 byte[] data; 87 try { 88 data = content.getBytes("UTF8"); 89 } catch (UnsupportedEncodingException e1) { 90 return null; 91 } 92 File fn = getHelpFileName(dir); 93 try (FileOutputStream os = new FileOutputStream(fn)) { 94 os.write(data); 95 } catch (IOException e) { 96 return null; 97 } 98 return fn; 99 } 100 readTemplate()101 private String readTemplate() { 102 Settings.Lang lang = Settings.Lang.byCode(langCode); 103 int resId = lang.helpFileResId; 104 if (resId == 0) 105 resId = Settings.Lang.DEFAULT.helpFileResId; 106 return engine.loadResourceUtf8(resId); 107 } 108 109 private enum MacroType { 110 IF(1), 111 ELSE(0), 112 ENDIF(0), 113 IMAGE(1), 114 SETTING(1), 115 ; 116 public final int paramCount; MacroType(int paramCount)117 private MacroType(int paramCount) { 118 this.paramCount = paramCount; 119 } byName(String name)120 static MacroType byName(String name) { 121 for (MacroType item : values()) { 122 if (item.name().equalsIgnoreCase(name)) 123 return item; 124 } 125 return null; 126 } 127 } 128 129 private static class MacroInfo { 130 public MacroType type; 131 public String param1; 132 public int len; 133 } 134 err(String msg)135 private static MacroInfo err(String msg) { 136 Log.e("cr3help", msg); 137 return null; 138 } 139 detectMacro(String s, int start)140 private static MacroInfo detectMacro(String s, int start) { 141 if (start + 11 > s.length()) 142 return null; 143 if (s.charAt(start + 0) != '<' 144 || s.charAt(start + 1) != '!' 145 ) 146 return null; 147 if (s.charAt(start + 2) != '-' 148 || s.charAt(start + 3) != '-' 149 ) 150 return null; 151 if (s.charAt(start + 4) != 'c' 152 || s.charAt(start + 5) != 'r' 153 || s.charAt(start + 6) != '3' 154 || s.charAt(start + 7) != ':' 155 ) 156 return null; 157 start += 8; 158 int end = s.indexOf("-->", start); 159 if (end < 0) 160 return err("unfinished macro"); 161 int len = (end - start) + 8 + 3; 162 String content = s.substring(start, end); 163 if (content.length() < 1) 164 return err("too short content: " + content); 165 String[] items = content.split(" "); 166 if (items.length < 1) 167 return err("invalid content: " + content); 168 MacroInfo res = new MacroInfo(); 169 res.type = MacroType.byName(items[0]); 170 if (res.type == null) 171 return err("unknown macro type: " + content); 172 if (res.type.paramCount > 0) { 173 if (items.length < 1) 174 return err("macro param missing: " + content); 175 res.param1 = items[1].trim(); 176 if (res.param1 == null || res.param1.length() == 0) 177 return err("macro param missing: " + content); 178 } 179 res.len = len; 180 return res; 181 } 182 getConditionValue(String conditionName)183 private boolean getConditionValue(String conditionName) { 184 int eqpos = conditionName.indexOf("=="); 185 int neqpos = conditionName.indexOf("!="); 186 String param = conditionName; 187 String value = null; 188 if (eqpos > 0) { 189 param = conditionName.substring(0, eqpos); 190 value = conditionName.substring(eqpos + 2); 191 } else if (neqpos > 0) { 192 param = conditionName.substring(0, neqpos); 193 value = conditionName.substring(neqpos + 2); 194 } 195 String prop = settings.getProperty(param); 196 if (prop == null) 197 return false; 198 if (eqpos > 0) 199 return prop.equals(value); 200 if (neqpos > 0) 201 return !prop.equals(value); 202 return "1".equals(prop); 203 } 204 205 private static class ImageRes { 206 public String name; 207 public int resourceId; ImageRes(String name, int resourceId)208 ImageRes(String name, int resourceId) { 209 this.name = name; 210 this.resourceId = resourceId; 211 } 212 } 213 private static final ImageRes[] images = { 214 new ImageRes("cr3_logo", R.mipmap.cr3_logo), 215 new ImageRes("open_file", R.drawable.ic_menu_archive), 216 new ImageRes("goto", R.drawable.ic_menu_goto), 217 new ImageRes("bookmarks", R.drawable.ic_menu_mark), 218 new ImageRes("select", android.R.drawable.ic_menu_edit), 219 new ImageRes("options", android.R.drawable.ic_menu_preferences), 220 new ImageRes("search", android.R.drawable.ic_menu_search), 221 }; 222 findImageResIdByName(String name)223 private static int findImageResIdByName(String name) { 224 for (ImageRes image : images) { 225 if (image.name.equalsIgnoreCase(name)) 226 return image.resourceId; 227 } 228 return 0; 229 } 230 231 private static final char ENCODE[] = { 232 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 233 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 234 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 235 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 236 }; 237 encodeImage(StringBuilder buf, byte[] data)238 private void encodeImage(StringBuilder buf, byte[] data) { 239 int i = 0; 240 for (; i <= data.length-3; i += 3) { 241 int v = ((data[i] & 0xFF) << 16) | ((data[i+1] & 0xFF) << 8) | (data[i+2] & 0xFF); 242 buf.append(ENCODE[(v >> 18) & 0x3f]); 243 buf.append(ENCODE[(v >> 12) & 0x3f]); 244 buf.append(ENCODE[(v >> 6) & 0x3f]); 245 buf.append(ENCODE[v & 0x3f]); 246 if (i / 3 % 16 == 15) 247 buf.append("\n"); 248 } 249 int tail = data.length - i; 250 if (tail == 1) { 251 int v = (data[i] & 0xff) << 4; 252 buf.append(ENCODE[(v >> 6) & 0x3f]); 253 buf.append(ENCODE[v & 0x3f]); 254 buf.append('='); 255 buf.append('='); 256 } else if (tail == 2) { 257 int v = ((data[i] & 0xff) << 10) | ((data[i] & 0xff) << 2); 258 buf.append(ENCODE[(v >> 12) & 0x3f]); 259 buf.append(ENCODE[(v >> 6) & 0x3f]); 260 buf.append(ENCODE[v & 0x3f]); 261 buf.append('='); 262 } 263 } 264 appendImage(String name, StringBuilder mainBuf, StringBuilder binBuf)265 private boolean appendImage(String name, StringBuilder mainBuf, StringBuilder binBuf) { 266 int res = findImageResIdByName(name); 267 if (res == 0) 268 return false; 269 byte[] data = engine.loadResourceBytes(res); 270 if (data == null || data.length == 0) 271 return false; 272 mainBuf.append("<image l:href=\"#" + name + ".png" + "\"/>"); 273 binBuf.append("\n<binary content-type=\"image/png\" id=\"" + name + ".png\">"); 274 encodeImage(binBuf, data); 275 binBuf.append("</binary>"); 276 return true; 277 } 278 getActionName(String actionId)279 private String getActionName(String actionId) { 280 ReaderAction a = ReaderAction.findById(actionId); 281 return context.getString(a.nameId); 282 } 283 getSettingValueName(String name)284 private String getSettingValueName(String name) { 285 if ("version".equals(name)) 286 return version; 287 String v = settings.getProperty(name); 288 if (v == null || v.length() == 0) 289 return null; 290 if (name.startsWith(Settings.PROP_APP_TAP_ZONE_ACTIONS_TAP)) 291 return getActionName(v); 292 if (name.startsWith(Settings.PROP_APP_KEY_ACTIONS_PRESS)) 293 return getActionName(v); 294 return v; 295 } 296 filterTemplate(String template)297 private String filterTemplate(String template) { 298 StringBuilder buf = new StringBuilder(template.length()); 299 StringBuilder binary = new StringBuilder(); 300 ArrayList<Boolean> ifStack = new ArrayList<Boolean>(); 301 boolean ifState = true; 302 for (int i=0; i<template.length(); i++) { 303 // <!--cr3:if condition--> (condition may be setting==value or setting!=value or setting (=="1", for bool values) 304 // <!--cr3:else --> else branch of if 305 // <!--cr3:endif --> end if 306 // <!--cr3:image name--> place image here 307 // <!--cr3:setting name--> 308 MacroInfo macro = null; 309 char ch = template.charAt(i); 310 if (ch == '<') 311 macro = detectMacro(template, i); 312 if (macro != null) { 313 boolean condValue = false; 314 switch (macro.type) { 315 case IF: 316 ifStack.add(ifState); 317 ifState = getConditionValue(macro.param1); 318 break; 319 case ELSE: 320 ifState = !ifState; 321 break; 322 case ENDIF: 323 if (ifStack.size() > 0) 324 ifState = ifStack.remove(ifStack.size() - 1); 325 break; 326 case IMAGE: 327 if (ifState) { 328 appendImage(macro.param1, buf, binary); 329 } 330 break; 331 case SETTING: 332 if (ifState) { 333 // TODO: show param value name here 334 String value = getSettingValueName(macro.param1); 335 if (value != null) 336 buf.append(getSettingValueName(macro.param1)); 337 } 338 break; 339 } 340 i += macro.len - 1; 341 } else { 342 if (ch == '<' && i >= template.length() - 20 && "</FictionBook>".equals(template.substring(i, i+14))) { 343 // before closing </FictionBook> tag put all binary (image) data 344 buf.append(binary); 345 } 346 if (ifState) 347 buf.append(ch); 348 } 349 } 350 return buf.toString(); 351 } 352 } 353