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