Java tutorial
/**************************************************************************************** * Copyright (c) 2009 Daniel Svrd <daniel.svard@gmail.com> * * Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com> * * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com> * * Copyright (c) 2012 Kostas Spyropoulos <inigo.aldana@gmail.com> * * * * 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 3 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, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.hichinaschool.flashcards.libanki; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.text.Html; import android.util.Log; import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; import com.hichinaschool.flashcards.anki.AnkiDb; import com.hichinaschool.flashcards.anki.AnkiDroidApp; import com.hichinaschool.flashcards.anki.AnkiFont; import com.hichinaschool.flashcards.anki.R; import com.mindprod.common11.BigDate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.nio.channels.FileChannel; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.Date; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.Enumeration; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Random; import java.util.TimeZone; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * TODO comments */ public class Utils { enum SqlCommandType { SQL_INS, SQL_UPD, SQL_DEL }; // Used to format doubles with English's decimal separator system public static final Locale ENGLISH_LOCALE = new Locale("en_US"); public static final int CHUNK_SIZE = 32768; private static final int DAYS_BEFORE_1970 = 719163; private static NumberFormat mCurrentNumberFormat; private static NumberFormat mCurrentPercentageFormat; private static TreeSet<Long> sIdTree; private static long sIdTime; private static final int TIME_SECONDS = 0; private static final int TIME_MINUTES = 1; private static final int TIME_HOURS = 2; private static final int TIME_DAYS = 3; private static final int TIME_MONTHS = 4; private static final int TIME_YEARS = 5; public static final int TIME_FORMAT_DEFAULT = 0; public static final int TIME_FORMAT_IN = 1; public static final int TIME_FORMAT_BEFORE = 2; // List of all extensions we accept as font files. private static final String[] FONT_FILE_EXTENSIONS = new String[] { ".ttf", ".ttc", ".otf" }; /* Prevent class from being instantiated */ private Utils() { } // Regex pattern used in removing tags from text before diff private static final Pattern stylePattern = Pattern.compile("(?s)<style.*?>.*?</style>"); private static final Pattern scriptPattern = Pattern.compile("(?s)<script.*?>.*?</script>"); private static final Pattern tagPattern = Pattern.compile("<.*?>"); private static final Pattern imgPattern = Pattern.compile("<img src=[\\\"']?([^\\\"'>]+)[\\\"']? ?/?>"); private static final Pattern htmlEntitiesPattern = Pattern.compile("&#?\\w+;"); private static final String ALL_CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; private static final String BASE91_EXTRA_CHARS = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"; public static final int FILE_COPY_BUFFER_SIZE = 2048; /**The time in integer seconds. Pass scale=1000 to get milliseconds. */ public static double now() { return (System.currentTimeMillis() / 1000.0); } /**The time in integer seconds. Pass scale=1000 to get milliseconds. */ public static long intNow() { return intNow(1); } public static long intNow(int scale) { return (long) (now() * scale); } // timetable // aftertimetable // shorttimetable /** * Return a string representing a time span (eg '2 days'). * @param inFormat: if true, return eg 'in 2 days' */ public static String fmtTimeSpan(int time) { return fmtTimeSpan(time, 0, false, false); } public static String fmtTimeSpan(int time, boolean _short) { return fmtTimeSpan(time, 0, _short, false); } public static String fmtTimeSpan(int time, int format, boolean _short, boolean boldNumber) { int type; int unit = 99; int point = 0; if (Math.abs(time) < 60 || unit < 1) { type = TIME_SECONDS; } else if (Math.abs(time) < 3600 || unit < 2) { type = TIME_MINUTES; } else if (Math.abs(time) < 60 * 60 * 24 || unit < 3) { type = TIME_HOURS; } else if (Math.abs(time) < 60 * 60 * 24 * 29.5 || unit < 4) { type = TIME_DAYS; } else if (Math.abs(time) < 60 * 60 * 24 * 30 * 11.95 || unit < 5) { type = TIME_MONTHS; point = 1; } else { type = TIME_YEARS; point = 1; } double ftime = convertSecondsTo(time, type); int formatId; if (false) {//_short) { //formatId = R.array.next_review_short; } else { switch (format) { case TIME_FORMAT_IN: if (Math.round(ftime * 10) == 10) { formatId = R.array.next_review_in_s; } else { formatId = R.array.next_review_in_p; } break; case TIME_FORMAT_BEFORE: if (Math.round(ftime * 10) == 10) { formatId = R.array.next_review_before_s; } else { formatId = R.array.next_review_before_p; } break; case TIME_FORMAT_DEFAULT: default: if (Math.round(ftime * 10) == 10) { formatId = R.array.next_review_s; } else { formatId = R.array.next_review_p; } break; } } String timeString = String.format(AnkiDroidApp.getAppResources().getStringArray(formatId)[type], boldNumber ? "<b>" + fmtDouble(ftime, point) + "</b>" : fmtDouble(ftime, point)); if (boldNumber && time == 1) { timeString = timeString.replace("1", "<b>1</b>"); } return timeString; } private static double convertSecondsTo(int seconds, int type) { switch (type) { case TIME_SECONDS: return seconds; case TIME_MINUTES: return seconds / 60.0; case TIME_HOURS: return seconds / 3600.0; case TIME_DAYS: return seconds / 86400.0; case TIME_MONTHS: return seconds / 2592000.0; case TIME_YEARS: return seconds / 31536000.0; default: return 0; } } /** * Locale * *********************************************************************************************** */ /** * @return double with percentage sign */ public static String fmtPercentage(Double value) { return fmtPercentage(value, 0); } public static String fmtPercentage(Double value, int point) { // only retrieve the percentage format the first time if (mCurrentPercentageFormat == null) { mCurrentPercentageFormat = NumberFormat.getPercentInstance(Locale.getDefault()); } mCurrentNumberFormat.setMaximumFractionDigits(point); return mCurrentPercentageFormat.format(value); } /** * @return a string with decimal separator according to current locale */ public static String fmtDouble(Double value) { return fmtDouble(value, 1); } public static String fmtDouble(Double value, int point) { // only retrieve the number format the first time if (mCurrentNumberFormat == null) { mCurrentNumberFormat = NumberFormat.getInstance(Locale.getDefault()); } mCurrentNumberFormat.setMaximumFractionDigits(point); return mCurrentNumberFormat.format(value); } /** * HTML * *********************************************************************************************** */ /** * Strips a text from <style>...</style>, <script>...</script> and <_any_tag_> HTML tags. * @param The HTML text to be cleaned. * @return The text without the aforementioned tags. */ public static String stripHTML(String s) { Matcher htmlMatcher = stylePattern.matcher(s); s = htmlMatcher.replaceAll(""); htmlMatcher = scriptPattern.matcher(s); s = htmlMatcher.replaceAll(""); htmlMatcher = tagPattern.matcher(s); s = htmlMatcher.replaceAll(""); return entsToTxt(s); } /** * Strip HTML but keep media filenames */ public static String stripHTMLMedia(String s) { Matcher imgMatcher = imgPattern.matcher(s); return stripHTML(imgMatcher.replaceAll(" $1 ")); } private String minimizeHTML(String s) { // TODO return s; } /** * Takes a string and replaces all the HTML symbols in it with their unescaped representation. * This should only affect substrings of the form &something; and not tags. * Internet rumour says that Html.fromHtml() doesn't cover all cases, but it doesn't get less * vague than that. * @param html The HTML escaped text * @return The text with its HTML entities unescaped. */ private static String entsToTxt(String html) { // entitydefs defines nbsp as \xa0 instead of a standard space, so we // replace it first html = html.replace(" ", " "); Matcher htmlEntities = htmlEntitiesPattern.matcher(html); StringBuffer sb = new StringBuffer(); while (htmlEntities.find()) { htmlEntities.appendReplacement(sb, Html.fromHtml(htmlEntities.group()).toString()); } htmlEntities.appendTail(sb); return sb.toString(); } /** * IDs * *********************************************************************************************** */ public static String hexifyID(long id) { return Long.toHexString(id); } public static long dehexifyID(String id) { return Long.valueOf(id, 16); } /** Given a list of integers, return a string '(int1,int2,...)'. */ public static String ids2str(int[] ids) { StringBuilder sb = new StringBuilder(); sb.append("("); if (ids != null) { String s = Arrays.toString(ids); sb.append(s.substring(1, s.length() - 1)); } sb.append(")"); return sb.toString(); } /** Given a list of integers, return a string '(int1,int2,...)'. */ public static String ids2str(long[] ids) { StringBuilder sb = new StringBuilder(); sb.append("("); if (ids != null) { String s = Arrays.toString(ids); sb.append(s.substring(1, s.length() - 1)); } sb.append(")"); return sb.toString(); } /** Given a list of integers, return a string '(int1,int2,...)'. */ public static String ids2str(Long[] ids) { StringBuilder sb = new StringBuilder(); sb.append("("); if (ids != null) { String s = Arrays.toString(ids); sb.append(s.substring(1, s.length() - 1)); } sb.append(")"); return sb.toString(); } /** Given a list of integers, return a string '(int1,int2,...)'. */ public static <T> String ids2str(List<T> ids) { StringBuilder sb = new StringBuilder(512); sb.append("("); boolean isNotFirst = false; for (T id : ids) { if (isNotFirst) { sb.append(", "); } else { isNotFirst = true; } sb.append(id); } sb.append(")"); return sb.toString(); } /** Given a list of integers, return a string '(int1,int2,...)'. */ public static String ids2str(JSONArray ids) { StringBuilder str = new StringBuilder(512); str.append("("); if (ids != null) { int len = ids.length(); for (int i = 0; i < len; i++) { try { if (i == (len - 1)) { str.append(ids.get(i)); } else { str.append(ids.get(i)).append(","); } } catch (JSONException e) { Log.e(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } } str.append(")"); return str.toString(); } /** LIBANKI: not in libanki */ public static long[] arrayList2array(List<Long> list) { long[] ar = new long[list.size()]; int i = 0; for (long l : list) { ar[i++] = l; } return ar; } /** Return a non-conflicting timestamp for table. */ public static long timestampID(AnkiDb db, String table) { // be careful not to create multiple objects without flushing them, or they // may share an ID. long t = intNow(1000); while (db.queryScalar("SELECT id FROM " + table + " WHERE id = " + t, false) != 0) { t += 1; } return t; } /** Return the first safe ID to use. */ public static long maxID(AnkiDb db) { long now = intNow(1000); now = Math.max(now, db.queryLongScalar("SELECT MAX(id) FROM cards")); now = Math.max(now, db.queryLongScalar("SELECT MAX(id) FROM notes")); return now + 1; } // used in ankiweb public static String base62(int num, String extra) { String table = ALL_CHARACTERS + extra; int len = table.length(); String buf = ""; int mod = 0; while (num != 0) { mod = num % len; buf = buf + table.substring(mod, mod + 1); num = num / len; } return buf; } // all printable characters minus quotes, backslash and separators public static String base91(int num) { return base62(num, BASE91_EXTRA_CHARS); } /** return a base91-encoded 64bit random number */ public static String guid64() { return base91((new Random()).nextInt((int) (Math.pow(2, 61) - 1))); } // increment a guid by one, for note type conflicts public static String incGuid(String guid) { return new StringBuffer(_incGuid(new StringBuffer(guid).reverse().toString())).reverse().toString(); } private static String _incGuid(String guid) { String table = ALL_CHARACTERS + BASE91_EXTRA_CHARS; int idx = table.indexOf(guid.substring(0, 1)); if (idx + 1 == table.length()) { // overflow guid = table.substring(0, 1) + _incGuid(guid.substring(1, guid.length())); } else { guid = table.substring(idx + 1) + guid.substring(1, guid.length()); } return guid; } // public static JSONArray listToJSONArray(List<Object> list) { // JSONArray jsonArray = new JSONArray(); // // for (Object o : list) { // jsonArray.put(o); // } // // return jsonArray; // } // // // public static List<String> jsonArrayToListString(JSONArray jsonArray) throws JSONException { // ArrayList<String> list = new ArrayList<String>(); // // int len = jsonArray.length(); // for (int i = 0; i < len; i++) { // list.add(jsonArray.getString(i)); // } // // return list; // } public static long[] jsonArrayToLongArray(JSONArray jsonArray) throws JSONException { long[] ar = new long[jsonArray.length()]; for (int i = 0; i < jsonArray.length(); i++) { ar[i] = jsonArray.getLong(i); } return ar; } /** * Fields * *********************************************************************************************** */ public static String joinFields(String[] list) { StringBuilder result = new StringBuilder(128); for (int i = 0; i < list.length - 1; i++) { result.append(list[i]).append("\u001f"); } if (list.length > 0) { result.append(list[list.length - 1]); } return result.toString(); } public static String[] splitFields(String fields) { // do not drop empty fields fields = fields.replaceAll("\\x1f\\x1f", "\u001f\u001e\u001f"); fields = fields.replaceAll("\\x1f$", "\u001f\u001e"); String[] split = fields.split("\\x1f"); for (int i = 0; i < split.length; i++) { if (split[i].matches("\\x1e")) { split[i] = ""; } } return split; } /** * Checksums * *********************************************************************************************** */ /** * SHA1 checksum. * Equivalent to python sha1.hexdigest() * * @param data the string to generate hash from * @return A string of length 40 containing the hexadecimal representation of the MD5 checksum of data. */ public static String checksum(String data) { String result = ""; if (data != null) { MessageDigest md = null; byte[] digest = null; try { md = MessageDigest.getInstance("SHA1"); digest = md.digest(data.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { Log.e(AnkiDroidApp.TAG, "Utils.checksum: No such algorithm. " + e.getMessage()); throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { Log.e(AnkiDroidApp.TAG, "Utils.checksum: " + e.getMessage()); e.printStackTrace(); } BigInteger biginteger = new BigInteger(1, digest); result = biginteger.toString(16); // pad with zeros to length of 40 This method used to pad // to the length of 32. As it turns out, sha1 has a digest // size of 160 bits, leading to a hex digest size of 40, // not 32. if (result.length() < 40) { String zeroes = "0000000000000000000000000000000000000000"; result = zeroes.substring(0, zeroes.length() - result.length()) + result; } } return result; } /** * @param data the string to generate hash from * @return 32 bit unsigned number from first 8 digits of sha1 hash */ public static long fieldChecksum(String data) { return Long.valueOf(checksum(stripHTMLMedia(data)).substring(0, 8), 16); } /** * Generate the SHA1 checksum of a file. * @param file The file to be checked * @return A string of length 32 containing the hexadecimal representation of the SHA1 checksum of the file's contents. */ public static String fileChecksum(String file) { byte[] buffer = new byte[1024]; byte[] digest = null; try { InputStream fis = new FileInputStream(file); MessageDigest md = MessageDigest.getInstance("SHA1"); int numRead = 0; do { numRead = fis.read(buffer); if (numRead > 0) { md.update(buffer, 0, numRead); } } while (numRead != -1); fis.close(); digest = md.digest(); } catch (FileNotFoundException e) { Log.e(AnkiDroidApp.TAG, "Utils.fileChecksum: File not found.", e); } catch (NoSuchAlgorithmException e) { Log.e(AnkiDroidApp.TAG, "Utils.fileChecksum: No such algorithm.", e); } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "Utils.fileChecksum: IO exception.", e); } BigInteger biginteger = new BigInteger(1, digest); String result = biginteger.toString(16); // pad with zeros to length of 40 - SHA1 is 160bit long if (result.length() < 40) { result = "0000000000000000000000000000000000000000".substring(0, 40 - result.length()) + result; } return result; } /** Replace HTML line break tags with new lines. */ public static String replaceLineBreak(String text) { return text.replaceAll("<br(\\s*\\/*)>", "\n"); } // /** // * MD5 sum of file. // * Equivalent to checksum(open(os.path.join(mdir, file), "rb").read())) // * // * @param path The full path to the file // * @return A string of length 32 containing the hexadecimal representation of the MD5 checksum of the contents // * of the file // */ // public static String fileChecksum(String path) { // byte[] bytes = null; // try { // File file = new File(path); // if (file != null && file.isFile()) { // bytes = new byte[(int)file.length()]; // FileInputStream fin = new FileInputStream(file); // fin.read(bytes); // } // } catch (FileNotFoundException e) { // Log.e(AnkiDroidApp.TAG, "Can't find file " + path + " to calculate its checksum"); // } catch (IOException e) { // Log.e(AnkiDroidApp.TAG, "Can't read file " + path + " to calculate its checksum"); // } // if (bytes == null) { // Log.w(AnkiDroidApp.TAG, "File " + path + " appears to be empty"); // return ""; // } // MessageDigest md = null; // byte[] digest = null; // try { // md = MessageDigest.getInstance("MD5"); // digest = md.digest(bytes); // } catch (NoSuchAlgorithmException e) { // Log.e(AnkiDroidApp.TAG, "Utils.checksum: No such algorithm. " + e.getMessage()); // throw new RuntimeException(e); // } // BigInteger biginteger = new BigInteger(1, digest); // String result = biginteger.toString(16); // // pad with zeros to length of 32 // if (result.length() < 32) { // result = "00000000000000000000000000000000".substring(0, 32 - result.length()) + result; // } // return result; // } /** * Tempo files * *********************************************************************************************** */ // tmpdir // tmpfile // namedtmp /** * Converts an InputStream to a String. * @param is InputStream to convert * @return String version of the InputStream */ public static String convertStreamToString(InputStream is) { String contentOfMyInputStream = ""; try { BufferedReader rd = new BufferedReader(new InputStreamReader(is), 4096); String line; StringBuilder sb = new StringBuilder(); while ((line = rd.readLine()) != null) { sb.append(line); } rd.close(); contentOfMyInputStream = sb.toString(); } catch (Exception e) { e.printStackTrace(); } return contentOfMyInputStream; } public static boolean unzipFiles(ZipFile zipFile, String targetDirectory, String[] zipEntries, HashMap<String, String> zipEntryToFilenameMap) { byte[] buf = new byte[FILE_COPY_BUFFER_SIZE]; File dir = new File(targetDirectory); if (!dir.exists() && !dir.mkdirs()) { Log.e(AnkiDroidApp.TAG, "Utils.unzipFiles: Could not create target directory: " + targetDirectory); return false; } if (zipEntryToFilenameMap == null) { zipEntryToFilenameMap = new HashMap<String, String>(); } BufferedInputStream zis = null; BufferedOutputStream bos = null; try { for (String requestedEntry : zipEntries) { ZipEntry ze = zipFile.getEntry(requestedEntry); if (ze != null) { String name = ze.getName(); if (zipEntryToFilenameMap.containsKey(name)) { name = zipEntryToFilenameMap.get(name); } File destFile = new File(dir, name); File parentDir = destFile.getParentFile(); if (!parentDir.exists() && !parentDir.mkdirs()) { return false; } if (!ze.isDirectory()) { // Log.i(AnkiDroidApp.TAG, "uncompress " + name); zis = new BufferedInputStream(zipFile.getInputStream(ze)); bos = new BufferedOutputStream(new FileOutputStream(destFile), FILE_COPY_BUFFER_SIZE); int n; while ((n = zis.read(buf, 0, FILE_COPY_BUFFER_SIZE)) != -1) { bos.write(buf, 0, n); } bos.flush(); bos.close(); zis.close(); } } } } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "Utils.unzipFiles: Error while unzipping archive.", e); return false; } finally { try { if (bos != null) { bos.close(); } } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "Utils.unzipFiles: Error while closing output stream.", e); } try { if (zis != null) { zis.close(); } } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "Utils.unzipFiles: Error while closing zip input stream.", e); } } return true; } /** * Compress data. * @param bytesToCompress is the byte array to compress. * @return a compressed byte array. * @throws java.io.IOException */ public static byte[] compress(byte[] bytesToCompress, int comp) throws IOException { // Compressor with highest level of compression. Deflater compressor = new Deflater(comp, true); // Give the compressor the data to compress. compressor.setInput(bytesToCompress); compressor.finish(); // Create an expandable byte array to hold the compressed data. // It is not necessary that the compressed data will be smaller than // the uncompressed data. ByteArrayOutputStream bos = new ByteArrayOutputStream(bytesToCompress.length); // Compress the data byte[] buf = new byte[65536]; while (!compressor.finished()) { bos.write(buf, 0, compressor.deflate(buf)); } bos.close(); // Get the compressed data return bos.toByteArray(); } /** * Utility method to write to a file. * Throws the exception, so we can report it in syncing log * @throws IOException */ public static void writeToFile(InputStream source, String destination) throws IOException { // Log.i(AnkiDroidApp.TAG, "Creating new file... = " + destination); new File(destination).createNewFile(); long startTimeMillis = System.currentTimeMillis(); OutputStream output = new BufferedOutputStream(new FileOutputStream(destination)); // Transfer bytes, from source to destination. byte[] buf = new byte[CHUNK_SIZE]; long sizeBytes = 0; int len; if (source == null) { Log.e(AnkiDroidApp.TAG, "source is null!"); } while ((len = source.read(buf)) >= 0) { output.write(buf, 0, len); sizeBytes += len; } long endTimeMillis = System.currentTimeMillis(); // Log.i(AnkiDroidApp.TAG, "Finished writing!"); long durationSeconds = (endTimeMillis - startTimeMillis) / 1000; long sizeKb = sizeBytes / 1024; long speedKbSec = 0; if (endTimeMillis != startTimeMillis) { speedKbSec = sizeKb * 1000 / (endTimeMillis - startTimeMillis); } // Log.d(AnkiDroidApp.TAG, "Utils.writeToFile: " + "Size: " + sizeKb + "Kb, " + "Duration: " + durationSeconds + "s, " + "Speed: " + speedKbSec + "Kb/s"); output.close(); } // Print methods public static void printJSONObject(JSONObject jsonObject) { printJSONObject(jsonObject, "-", null); } public static void printJSONObject(JSONObject jsonObject, boolean writeToFile) { BufferedWriter buff; try { buff = writeToFile ? new BufferedWriter(new FileWriter("/sdcard/payloadAndroid.txt"), 8192) : null; try { printJSONObject(jsonObject, "-", buff); } finally { if (buff != null) buff.close(); } } catch (IOException ioe) { Log.e(AnkiDroidApp.TAG, "IOException = " + ioe.getMessage()); } } private static void printJSONObject(JSONObject jsonObject, String indentation, BufferedWriter buff) { try { @SuppressWarnings("unchecked") Iterator<String> keys = (Iterator<String>) jsonObject.keys(); TreeSet<String> orderedKeysSet = new TreeSet<String>(); while (keys.hasNext()) { orderedKeysSet.add(keys.next()); } Iterator<String> orderedKeys = orderedKeysSet.iterator(); while (orderedKeys.hasNext()) { String key = orderedKeys.next(); try { Object value = jsonObject.get(key); if (value instanceof JSONObject) { if (buff != null) { buff.write(indentation + " " + key + " : "); buff.newLine(); } // Log.i(AnkiDroidApp.TAG, " " + indentation + key + " : "); printJSONObject((JSONObject) value, indentation + "-", buff); } else { if (buff != null) { buff.write(indentation + " " + key + " = " + jsonObject.get(key).toString()); buff.newLine(); } // Log.i(AnkiDroidApp.TAG, " " + indentation + key + " = " + jsonObject.get(key).toString()); } } catch (JSONException e) { Log.e(AnkiDroidApp.TAG, "JSONException = " + e.getMessage()); } } } catch (IOException e1) { Log.e(AnkiDroidApp.TAG, "IOException = " + e1.getMessage()); } } /* public static void saveJSONObject(JSONObject jsonObject) throws IOException { // Log.i(AnkiDroidApp.TAG, "saveJSONObject"); BufferedWriter buff = new BufferedWriter(new FileWriter("/sdcard/jsonObjectAndroid.txt", true)); buff.write(jsonObject.toString()); buff.close(); } */ /** * Returns 1 if true, 0 if false * * @param b The boolean to convert to integer * @return 1 if b is true, 0 otherwise */ public static int booleanToInt(boolean b) { return (b) ? 1 : 0; } /** * Returns the effective date of the present moment. * If the time is prior the cut-off time (9:00am by default as of 11/02/10) return yesterday, * otherwise today * Note that the Date class is java.sql.Date whose constructor sets hours, minutes etc to zero * * @param utcOffset The UTC offset in seconds we are going to use to determine today or yesterday. * @return The date (with time set to 00:00:00) that corresponds to today in Anki terms */ public static Date genToday(double utcOffset) { // The result is not adjusted for timezone anymore, following libanki model // Timezone adjustment happens explicitly in Deck.updateCutoff(), but not in Deck.checkDailyStats() SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); df.setTimeZone(TimeZone.getTimeZone("GMT")); Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); cal.setTimeInMillis(System.currentTimeMillis() - (long) utcOffset * 1000l); Date today = Date.valueOf(df.format(cal.getTime())); return today; } public static void printDate(String name, double date) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss"); df.setTimeZone(TimeZone.getTimeZone("GMT")); Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); cal.setTimeInMillis((long) date * 1000); // Log.d(AnkiDroidApp.TAG, "Value of " + name + ": " + cal.getTime().toGMTString()); } public static String doubleToTime(double value) { int time = (int) Math.round(value); int seconds = time % 60; int minutes = (time - seconds) / 60; String formattedTime; if (seconds < 10) { formattedTime = Integer.toString(minutes) + ":0" + Integer.toString(seconds); } else { formattedTime = Integer.toString(minutes) + ":" + Integer.toString(seconds); } return formattedTime; } /** * Returns the proleptic Gregorian ordinal of the date, where January 1 of year 1 has ordinal 1. * @param date Date to convert to ordinal, since 01/01/01 * @return The ordinal representing the date */ public static int dateToOrdinal(Date date) { // BigDate.toOrdinal returns the ordinal since 1970, so we add up the days from 01/01/01 to 1970 return BigDate.toOrdinal(date.getYear() + 1900, date.getMonth() + 1, date.getDate()) + DAYS_BEFORE_1970; } /** * Return the date corresponding to the proleptic Gregorian ordinal, where January 1 of year 1 has ordinal 1. * @param ordinal representing the days since 01/01/01 * @return Date converted from the ordinal */ public static Date ordinalToDate(int ordinal) { return new Date((new BigDate(ordinal - DAYS_BEFORE_1970)).getLocalDate().getTime()); } /** * Indicates whether the specified action can be used as an intent. This method queries the package manager for * installed packages that can respond to an intent with the specified action. If no suitable package is found, this * method returns false. * @param context The application's environment. * @param action The Intent action to check for availability. * @return True if an Intent with the specified action can be sent and responded to, false otherwise. */ public static boolean isIntentAvailable(Context context, String action) { return isIntentAvailable(context, action, null); } public static boolean isIntentAvailable(Context context, String action, ComponentName componentName) { final PackageManager packageManager = context.getPackageManager(); final Intent intent = new Intent(action); intent.setComponent(componentName); List<ResolveInfo> list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return list.size() > 0; } /** * @param mediaDir media directory path on SD card * @return path converted to file URL, properly UTF-8 URL encoded */ public static String getBaseUrl(String mediaDir) { // Use android.net.Uri class to ensure whole path is properly encoded // File.toURL() does not work here, and URLEncoder class is not directly usable // with existing slashes if (mediaDir.length() != 0 && !mediaDir.equalsIgnoreCase("null")) { Uri mediaDirUri = Uri.fromFile(new File(mediaDir)); return mediaDirUri.toString() + "/"; } return ""; } /** * Take an array of Long and return an array of long * * @param array The input with type Long[] * @return The output with type long[] */ public static long[] toPrimitive(Long[] array) { long[] results = new long[array.length]; if (array != null) { for (int i = 0; i < array.length; i++) { results[i] = array[i].longValue(); } } return results; } public static long[] toPrimitive(Collection<Long> array) { long[] results = new long[array.size()]; if (array != null) { int i = 0; for (Long item : array) { results[i++] = item.longValue(); } } return results; } public static void updateProgressBars(View view, int x, int y) { if (view == null) { return; } if (view.getParent() instanceof LinearLayout) { LinearLayout.LayoutParams lparam = new LinearLayout.LayoutParams(0, 0); lparam.height = y; lparam.width = x; view.setLayoutParams(lparam); } else if (view.getParent() instanceof FrameLayout) { FrameLayout.LayoutParams lparam = new FrameLayout.LayoutParams(0, 0); lparam.height = y; lparam.width = x; view.setLayoutParams(lparam); } } /** * Calculate the UTC offset */ public static double utcOffset() { Calendar cal = Calendar.getInstance(); // 4am return 4 * 60 * 60 - (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000; } /** Returns the filename without the extension. */ public static String removeExtension(String filename) { int dotPosition = filename.lastIndexOf('.'); if (dotPosition == -1) { return filename; } return filename.substring(0, dotPosition); } /** Returns only the filename extension. */ public static String getFileExtension(String filename) { int dotPosition = filename.lastIndexOf('.'); if (dotPosition == -1) { return ""; } return filename.substring(dotPosition); } /** Removes any character that are not valid as deck names. */ public static String removeInvalidDeckNameCharacters(String name) { if (name == null) { return null; } // The only characters that we cannot absolutely allow to appear in the filename are the ones reserved in some // file system. Currently these are \, /, and :, in order to cover Linux, OSX, and Windows. return name.replaceAll("[:/\\\\]", ""); } /** Returns a list of files for the installed custom fonts. */ public static List<AnkiFont> getCustomFonts(Context context) { String deckPath = AnkiDroidApp.getCurrentAnkiDroidDirectory(); String fontsPath = deckPath + "/fonts/"; File fontsDir = new File(fontsPath); int fontsCount = 0; File[] fontsList = null; if (fontsDir.exists() && fontsDir.isDirectory()) { fontsCount = fontsDir.listFiles().length; fontsList = fontsDir.listFiles(); } String[] ankiDroidFonts = null; try { ankiDroidFonts = context.getAssets().list("fonts"); } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "Error on retrieving ankidroid fonts: " + e); } List<AnkiFont> fonts = new ArrayList<AnkiFont>(); for (int i = 0; i < fontsCount; i++) { String filePath = fontsList[i].getAbsolutePath(); String filePathExtension = getFileExtension(filePath); for (String fontExtension : FONT_FILE_EXTENSIONS) { // Go through the list of allowed extensios. if (filePathExtension.equalsIgnoreCase(fontExtension)) { // This looks like a font file. AnkiFont font = AnkiFont.createAnkiFont(context, filePath, false); if (font != null) { fonts.add(font); } break; // No need to look for other file extensions. } } } for (int i = 0; i < ankiDroidFonts.length; i++) { // Assume all files in the assets directory are actually fonts. AnkiFont font = AnkiFont.createAnkiFont(context, ankiDroidFonts[i], true); if (font != null) { fonts.add(font); } } return fonts; } /** Returns a list of apkg-files. */ public static List<File> getImportableDecks() { String deckPath = AnkiDroidApp.getCurrentAnkiDroidDirectory(); File dir = new File(deckPath); int deckCount = 0; File[] deckList = null; if (dir.exists() && dir.isDirectory()) { deckList = dir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { if (pathname.isFile() && pathname.getName().endsWith(".apkg")) { return true; } return false; } }); deckCount = deckList.length; } List<File> decks = new ArrayList<File>(); for (int i = 0; i < deckCount; i++) { decks.add(deckList[i]); } return decks; } /** Joins the given string values using the delimiter between them. */ public static String join(String delimiter, String... values) { StringBuilder sb = new StringBuilder(); for (String value : values) { if (sb.length() != 0) { sb.append(delimiter); } sb.append(value); } return sb.toString(); } /** * Simply copy a file to another location * @param sourceFile The source file * @param destFile The destination file, doesn't need to exist yet. * @throws IOException */ public static void copyFile(File sourceFile, File destFile) throws IOException { if (!destFile.exists()) { destFile.createNewFile(); } FileChannel source = null; FileChannel destination = null; try { source = new FileInputStream(sourceFile).getChannel(); destination = new FileOutputStream(destFile).getChannel(); destination.transferFrom(source, 0, source.size()); } finally { if (source != null) { source.close(); } if (destination != null) { destination.close(); } } } /** * Like org.json.JSONObject except that it doesn't escape forward slashes * The necessity for this method is due to python's 2.7 json.dumps() function that doesn't escape chracter '/'. * The org.json.JSONObject parser accepts both escaped and unescaped forward slashes, so we only need to worry for * our output, when we write to the database or syncing. * * @param json a json object to serialize * @return the json serialization of the object * @see org.json.JSONObject#toString() */ public static String jsonToString(JSONObject json) { return json.toString().replaceAll("\\\\/", "/"); } /** * Like org.json.JSONArray except that it doesn't escape forward slashes * The necessity for this method is due to python's 2.7 json.dumps() function that doesn't escape chracter '/'. * The org.json.JSONArray parser accepts both escaped and unescaped forward slashes, so we only need to worry for * our output, when we write to the database or syncing. * * @param json a json object to serialize * @return the json serialization of the object * @see org.json.JSONArray#toString() */ public static String jsonToString(JSONArray json) { return json.toString().replaceAll("\\\\/", "/"); } /** * @return A description of the device, including the model and android version. No commas are present in the * returned string. */ public static String platDesc() { // AnkiWeb reads this string and uses , and : as delimiters, so we remove them. String model = android.os.Build.MODEL.replace(',', ' ').replace(':', ' '); return String.format(Locale.US, "android:%s:%s", android.os.Build.VERSION.RELEASE, model); } }