me.piebridge.bible.Bible.java Source code

Java tutorial

Introduction

Here is the source code for me.piebridge.bible.Bible.java

Source

/*
 * vim: set sw=4 ts=4:
 *
 * Copyright (C) 2013 Liu DongMiao <thom@piebridge.me>
 *
 * This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the Do What The Fuck You Want
 * To Bible License, Version 2, as published by Sam Hocevar. See
 * http://sam.zoy.org/wtfpl/COPYING for more details.
 *
 */

package me.piebridge.bible;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;

public class Bible {

    private final static String TAG = "me.piebridge.bible$Bible";

    public enum TYPE {
        VERSION, CHAPTER, BOOK, OSIS, HUMAN, VERSIONPATH,
    }

    private SQLiteDatabase database = null;
    private String databaseVersion = "";

    private static Context mContext = null;

    private ArrayList<String> books = new ArrayList<String>();
    private ArrayList<String> osiss = new ArrayList<String>();
    private ArrayList<String> chapters = new ArrayList<String>();
    private ArrayList<String> versions = new ArrayList<String>();
    private Object versionsLock = new Object();
    private Object versionsCheckingLock = new Object();
    private HashMap<String, String> versionpaths = new HashMap<String, String>();
    private ArrayList<String> humans = new ArrayList<String>();

    private static Bible bible = null;

    private HashMap<String, String> versionNames = new HashMap<String, String>();
    private HashMap<String, String> versionDates = new HashMap<String, String>();
    private HashMap<String, String> versionFullnames = new HashMap<String, String>();
    private HashMap<String, String> annotations = new HashMap<String, String>();

    private LinkedHashMap<String, String> allhuman = new LinkedHashMap<String, String>();
    private LinkedHashMap<String, String> allosis = new LinkedHashMap<String, String>();
    private LinkedHashMap<String, String> searchfull = new LinkedHashMap<String, String>();;
    private LinkedHashMap<String, String> searchshort = new LinkedHashMap<String, String>();;

    private Collator collator;
    private Locale lastLocale;
    private boolean unpacked = false;
    private HashMap<String, Long> mtime = new HashMap<String, Long>();
    private String css;
    public String versionName;

    private static String HUMAN_PREFERENCE = "human";

    private Bible(Context context) {
        Log.d(TAG, "init bible");
        mContext = context;
        SharedPreferences preferences = mContext.getSharedPreferences(HUMAN_PREFERENCE, Context.MODE_MULTI_PROCESS);
        for (Entry<String, ?> entry : preferences.getAll().entrySet()) {
            allhuman.put(entry.getKey(), String.valueOf(entry.getValue()));
        }
        try {
            versionName = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
        } catch (NameNotFoundException e) {
        }
        checkLocale();
        setDefaultVersion();
    }

    public void checkLocale() {
        Locale locale = Locale.getDefault();
        collator = Collator.getInstance(Locale.getDefault());
        if (!locale.equals(lastLocale)) {
            lastLocale = locale;
            setResources();
        }
    }

    public synchronized static Bible getBible(Context context) {
        if (bible == null) {
            bible = new Bible(context);
        }
        if (context != null) {
            mContext = context;
            bible.checkLocale();
        }
        return bible;
    }

    public int[] getChapterVerse(String string) {
        int value;
        try {
            value = Integer.parseInt(string);
        } catch (Exception e) {
            value = 0;
        }
        int chapter = value / 1000;
        int verse = value - chapter * 1000;
        return new int[] { chapter, verse };
    }

    private int checkVersionsSync(boolean all) {
        List<String> newVersions = new ArrayList<String>();
        Map<String, String> newVersionpaths = new HashMap<String, String>();
        File path = getExternalFilesDirWrapper();
        if (path == null) {
            checkInternalVersions(newVersions, newVersionpaths, all);
            synchronized (versionsLock) {
                mtime.clear();
                versions.clear();
                versionpaths.clear();
                versions.addAll(newVersions);
                versionpaths.putAll(newVersionpaths);
            }
            return versions.size();
        }
        File oldpath = new File(Environment.getExternalStorageDirectory(), ".piebridge");
        Long oldmtime = mtime.get(path.getAbsolutePath());
        if (oldmtime == null) {
            oldmtime = 0L;
        }
        if (versions.size() != 0 && path.lastModified() <= oldmtime
                && (!oldpath.exists() || !oldpath.isDirectory() || oldpath.lastModified() <= oldmtime)) {
            return versions.size();
        }
        checkVersion(oldpath, newVersions, newVersionpaths, all);
        checkVersion(path, newVersions, newVersionpaths, all);
        Collections.sort(newVersions, new Comparator<String>() {
            @Override
            public int compare(String item1, String item2) {
                return collator.compare(getVersionFullname(item1), getVersionFullname(item2));
            }
        });
        if (newVersions.size() == 0) {
            checkInternalVersions(newVersions, newVersionpaths, all);
        }
        synchronized (versionsLock) {
            mtime.put(path.getAbsolutePath(), path.lastModified());
            versions.clear();
            versionpaths.clear();
            versions.addAll(newVersions);
            versionpaths.putAll(newVersionpaths);
        }
        return versions.size();
    }

    private void checkInternalVersions(List<String> versions, Map<String, String> versionpaths, boolean all) {
        if (!unpacked) {
            setDemoVersions();
            unpacked = true;
        }
        checkVersion(mContext.getFilesDir(), versions, versionpaths, all);
    }

    public boolean isDemoVersion(String version) {
        File file = getFile(version);
        if (file == null) {
            return false;
        } else {
            return file.getParentFile().equals(mContext.getFilesDir());
        }
    }

    public boolean setVersion(String version) {
        if (version == null) {
            return false;
        }
        File file = getFile(version);
        if (file == null || !file.isFile()) {
            if ("".equals(databaseVersion)) {
                return setDefaultVersion();
            } else {
                return false;
            }
        }
        if (database != null) {
            if (databaseVersion.equals(version)) {
                return true;
            }
            Log.d(TAG, "close database \"" + database.getPath() + "\"");
            database.close();
        }
        databaseVersion = version;
        try {
            database = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null,
                    SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
            Log.d(TAG, "open database \"" + database.getPath() + "\"");
            int oldsize = allhuman.size();
            setMetadata(database, databaseVersion, true);
            if (allhuman.size() > oldsize) {
                SharedPreferences.Editor editor = mContext
                        .getSharedPreferences(HUMAN_PREFERENCE, Context.MODE_MULTI_PROCESS).edit();
                for (Entry<String, String> entry : allhuman.entrySet()) {
                    editor.putString(entry.getKey(), entry.getValue());
                }
                editor.commit();
            }
            return true;
        } catch (Exception e) {
            try {
                file.delete();
            } catch (Exception f) {
            }
            return setDefaultVersion();
        }
    }

    private File getFile(File dir, String version) {
        Log.d(TAG, "directory: " + dir + ", version: " + version);
        if (dir == null || !dir.isDirectory()) {
            return null;
        }
        File path = new File(dir, version + ".sqlite3");
        if (path == null || !path.isFile()) {
            return null;
        }
        return path;
    }

    private File getFile(String version) {
        if (version == null) {
            return null;
        }
        version = version.toLowerCase(Locale.US);
        String path = versionpaths.get(version);
        if (path != null) {
            return new File(path);
        } else {
            File file;
            file = getFile(getExternalFilesDirWrapper(), version);
            Log.d(TAG, "version: " + version);
            if (file != null) {
                versionpaths.put(version, file.getAbsolutePath());
                return file;
            }
            file = getFile(new File(Environment.getExternalStorageDirectory(), ".piebridge"), version);
            if (file != null) {
                versionpaths.put(version, file.getAbsolutePath());
                return file;
            }
            return null;
        }
    }

    private void setMetadata(SQLiteDatabase metadata, String dataversion, boolean change) {
        Cursor cursor = metadata.query(Provider.TABLE_BOOKS, Provider.COLUMNS_BOOKS, null, null, null, null, null);
        if (change) {
            osiss.clear();
            books.clear();
            chapters.clear();
            humans.clear();
        }
        try {
            while (cursor.moveToNext()) {
                String osis = cursor.getString(cursor.getColumnIndexOrThrow(Provider.COLUMN_OSIS));
                String book = cursor.getString(cursor.getColumnIndexOrThrow(Provider.COLUMN_HUMAN));
                String chapter = cursor.getString(cursor.getColumnIndexOrThrow(Provider.COLUMN_CHAPTERS));

                if (book.endsWith(" 1")) {
                    book = book.substring(0, book.length() - 2);
                }
                if (!allhuman.containsKey(book)) {
                    allhuman.put(book, osis);
                }

                Cursor cursor_chapter = null;
                // select group_concat(replace(reference_osis, "Gen.", "")) as osis from chapters where reference_osis like 'Gen.%';
                try {
                    cursor_chapter = metadata.query(Provider.TABLE_CHAPTERS,
                            new String[] {
                                    "group_concat(replace(reference_osis, \"" + osis + ".\", \"\")) as osis" },
                            "reference_osis like ?", new String[] { osis + ".%" }, null, null, null);
                    if (cursor_chapter.moveToNext()) {
                        // we have only one column
                        chapter = cursor_chapter.getString(0);
                    }
                } catch (Exception e) {
                } finally {
                    if (cursor_chapter != null) {
                        cursor_chapter.close();
                    }
                }
                if (change) {
                    osiss.add(osis);
                    books.add(book);
                    chapters.add(chapter);
                    humans.add(book);
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        css = getVersionMetadata("css", metadata, "");
    }

    public String getCSS() {
        return css;
    }

    private File getExternalFilesDir() {
        // /mnt/sdcard/Android/data/me.piebridge.bible/files
        File file = new File(
                new File(new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"),
                        mContext.getPackageName()),
                "files");
        if (!file.exists()) {
            if (!file.mkdirs()) {
                Log.w(TAG, "cannot create directory: " + file);
                return null;
            }
            try {
                (new File(file, ".nomedia")).createNewFile();
            } catch (java.io.IOException ioe) {
            }
        }
        return file;
    }

    private File getExternalFilesDirWrapper() {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) || mContext == null) {
            Log.d(TAG, "not mounted, mContext = " + mContext);
            return null;
        }
        try {
            Method method = Context.class.getMethod("getExternalFilesDir", new Class[] { String.class });
            return (File) method.invoke(mContext, new Object[] { null });
        } catch (Exception e) {
            Log.d(TAG, "internal getExternalFilesDir");
            return getExternalFilesDir();
        }
    }

    public ArrayList<String> get(TYPE type) {
        synchronized (versionsLock) {
            return getSync(type);
        }
    }

    public ArrayList<String> getSync(TYPE type) {
        switch (type) {
        case VERSION:
            return versions;
        case CHAPTER:
            return chapters;
        case BOOK:
            return books;
        case OSIS:
            return osiss;
        case HUMAN:
            return humans;
        case VERSIONPATH:
            return new ArrayList<String>(versionpaths.keySet());
        default:
            return new ArrayList<String>();
        }
    }

    public String getDate(String version) {
        return versionDates.get(version);
    }

    public int getPosition(TYPE type, String string) {
        return get(type).indexOf(string);
    }

    public String get(TYPE type, int pos) {
        ArrayList<String> arrayList = get(type);
        if (pos == -1) {
            pos = arrayList.size() - 1;
        }
        if (pos > -1 && pos < arrayList.size()) {
            return arrayList.get(pos);
        } else {
            return null;
        }
    }

    public int getCount(TYPE type) {
        return get(type).size();
    }

    public SQLiteDatabase getDatabase() {
        return database;
    }

    public String getVersion() {
        if (databaseVersion.equals("")) {
            setDefaultVersion();
        }
        return databaseVersion;
    }

    private String getVersionMetadata(String name, SQLiteDatabase metadata, String defaultValue) {
        String value = defaultValue;
        Cursor cursor = metadata.query("metadata", new String[] { "value" }, "name = ? or name = ?",
                new String[] { name, name + "_" + Locale.getDefault().toString() }, null, null, "name desc", "1");
        while (cursor != null && cursor.moveToNext()) {
            value = cursor.getString(cursor.getColumnIndexOrThrow("value"));
            break;
        }
        if (cursor != null) {
            cursor.close();
        }
        return value;
    }

    public String getVersionFullname(String version) {
        version = version.toLowerCase(Locale.US);
        String fullname = getResourceValue(versionFullnames,
                version.replace("demo", "").replace("niv84", "niv1984"));
        if (version.endsWith("demo")) {
            fullname += "(" + mContext.getString(R.string.demo) + ")";
        }
        return fullname;
    }

    public String getVersionName(String version) {
        version = version.toLowerCase(Locale.US);
        return getResourceValue(versionNames, version.replace("demo", "").replace("niv84", "niv1984"))
                .toUpperCase(Locale.US);
    }

    private String getResourceValue(HashMap<String, String> map, String key) {
        return map.containsKey(key) ? map.get(key) : key;
    }

    private void setResourceValues(HashMap<String, String> map, int resId) {
        map.clear();
        for (String entry : mContext.getResources().getStringArray(resId)) {
            String[] strings = entry.split("\\|", 2);
            map.put(strings[0], strings[1]);
        }
    }

    private void setResourceValuesReverse(HashMap<String, String> map, int resId) {
        for (String entry : mContext.getResources().getStringArray(resId)) {
            String[] strings = entry.split("\\|", 2);
            if (strings.length > 1) {
                map.put(strings[1], strings[0]);
            }
        }
    }

    private void setResources() {
        Log.d(TAG, "setResources");
        setResourceValues(versionNames, R.array.versionname);
        setResourceValues(versionFullnames, R.array.versionfullname);
        setResourceValuesReverse(allosis, R.array.osiszhcn);
        setResourceValuesReverse(allosis, R.array.osiszhtw);
        setResourceValuesReverse(searchfull, R.array.searchfullzhcn);
        setResourceValuesReverse(searchshort, R.array.searchshortzhcn);
    }

    private void setDemoVersions() {
        int demoVersion = PreferenceManager.getDefaultSharedPreferences(mContext).getInt("demoVersion", 0);
        int versionCode = 0;
        try {
            versionCode = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionCode;
        } catch (Throwable t) {
        }
        boolean newVersion = (demoVersion != versionCode);
        boolean unpack = unpackRaw(newVersion, R.raw.niv84demo,
                new File(mContext.getFilesDir(), "niv84demo.sqlite3"));
        if (unpack) {
            unpack = unpackRaw(newVersion, R.raw.cunpssdemo,
                    new File(mContext.getFilesDir(), "cunpssdemo.sqlite3"));
        }
        if (newVersion && unpack) {
            PreferenceManager.getDefaultSharedPreferences(mContext).edit().putInt("demoVersion", versionCode)
                    .commit();
        }
    }

    public boolean setDefaultVersion() {
        String defaultVersion = "";
        if (versions.size() > 0) {
            defaultVersion = get(TYPE.VERSION, 0);
        }
        String version = PreferenceManager.getDefaultSharedPreferences(mContext).getString("version",
                defaultVersion);
        if ((version == null || version.length() == 0) && defaultVersion.length() > 0) {
            version = defaultVersion;
        }
        // check actual file
        File file = getFile(version);
        if (file != null && file.isFile()) {
            return setVersion(version);
        }
        PreferenceManager.getDefaultSharedPreferences(mContext).edit().remove("version").commit();
        if (versions.size() == 0) {
            checkBibleData(false);
        }
        if (versions.size() > 0) {
            return setDefaultVersion();
        }
        return false;
    }

    private boolean unpackRaw(boolean newVersion, int resId, File file) {
        if (file.exists()) {
            if (!newVersion) {
                return true;
            }
            file.delete();
        }

        Log.d(TAG, "unpacking " + file.getAbsolutePath());

        try {
            int length;
            byte[] buffer = new byte[8192];
            OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
            InputStream is = mContext.getResources().openRawResource(resId);
            while ((length = is.read(buffer)) >= 0) {
                os.write(buffer, 0, length);
            }
            is.close();
            os.close();
        } catch (Exception e) {
            Log.e(TAG, "unpacked " + file.getAbsolutePath(), e);
            return false;
        }

        return true;
    }

    private String getOsis(String book, ArrayList<LinkedHashMap<String, String>> maps) {

        // test osis
        if (maps.size() > 0) {
            for (String osis : maps.get(0).values()) {
                if (osis.equalsIgnoreCase(book)) {
                    return osis;
                }
            }
        }

        book = book.toLowerCase(Locale.US).replace(" ", "");
        // prefer fullname
        for (LinkedHashMap<String, String> map : maps) {
            for (Entry<String, String> entry : map.entrySet()) {
                if (entry.getKey().toLowerCase(Locale.US).replace(" ", "").equals(book)) {
                    return entry.getValue();
                }
            }
        }

        // prefer startswith
        for (LinkedHashMap<String, String> map : maps) {
            for (Entry<String, String> entry : map.entrySet()) {
                if (entry.getKey().toLowerCase(Locale.US).replace(" ", "").startsWith(book)) {
                    return entry.getValue();
                }
            }
        }

        // prefer contains
        for (LinkedHashMap<String, String> map : maps) {
            for (Entry<String, String> entry : map.entrySet()) {
                if (entry.getKey().toLowerCase(Locale.US).replace(" ", "").contains(book)) {
                    return entry.getValue();
                }
            }
        }

        return null;
    }

    private ArrayList<LinkedHashMap<String, String>> getMaps(TYPE type) {
        checkLocale();
        ArrayList<LinkedHashMap<String, String>> maps = new ArrayList<LinkedHashMap<String, String>>();
        if (type == TYPE.HUMAN) {
            maps.add(allhuman);
            maps.add(searchfull);
            maps.add(searchshort);
        } else {
            maps.add(allosis);
        }
        return maps;
    }

    /*
     * ?book?osis
     *
     */
    public String getOsis(String book) {
        String osis;

        if (book == null || "".equals(book)) {
            return null;
        }

        book = book.replace("", "");
        book = book.replace("", "");
        book = book.replace("", "??");
        book = book.toLowerCase(Locale.US);
        book = book.replace("psalms", "psalm");

        boolean checkOsis = true;
        if (isCJK(book.substring(0, 1)) && book.length() > 2) {
            checkOsis = false;
        } else if (book.length() > 6) { // max 1Thess, 2Thess
            checkOsis = false;
        }

        Log.d(TAG, "getOsis, book: " + book);

        if (checkOsis) {
            osis = getOsis(book, getMaps(TYPE.OSIS));
            if (osis != null) {
                return osis;
            }
        }

        osis = getOsis(book, getMaps(TYPE.HUMAN));
        if (osis != null) {
            return osis;
        }

        return null;
    }

    private boolean checkStartSuggest(LinkedHashMap<String, String> osiss, String value, String key, String book,
            int limit) {
        if ("".equals(book) || value.replace(" ", "").toLowerCase(Locale.US).startsWith(book)) {
            if (addSuggest(osiss, value, key, limit)) {
                return true;
            }
        }
        return false;
    }

    private boolean checkContainSuggest(LinkedHashMap<String, String> osiss, String value, String key, String book,
            int limit) {
        if (value.replace(" ", "").toLowerCase(Locale.US).contains(book)) {
            if (addSuggest(osiss, value, key, limit)) {
                return true;
            }
        }
        return false;
    }

    private boolean addSuggest(LinkedHashMap<String, String> osiss, String value, String osis, int limit) {
        if (!osiss.values().contains(osis)) {
            String text = get(TYPE.HUMAN, bible.getPosition(TYPE.OSIS, osis));
            Log.d(TAG, "add suggest, text=" + text + ", data=" + osis);
            osiss.put(text, osis);
        }
        if (limit != -1 && osiss.size() >= limit) {
            Log.d(TAG, "arrive limit " + limit);
            return true;
        }
        return false;
    }

    /*
     * ?book?osiss?
     *
     */
    public LinkedHashMap<String, String> getOsiss(String book, int limit) {
        LinkedHashMap<String, String> osiss = new LinkedHashMap<String, String>();

        if (book != null) {
            // fix for zhcn
            book = book.replace("", "");
            book = book.replace("", "");
            book = book.replace("", "??");
            book = book.replace(SearchManager.SUGGEST_URI_PATH_QUERY, "").replace(" ", "").toLowerCase(Locale.US);
        }

        Log.d(TAG, "book: " + book);

        ArrayList<Entry<String, String>> maps = new ArrayList<Entry<String, String>>();

        for (Entry<String, String> entry : searchshort.entrySet()) {
            maps.add(entry);
        }

        for (Entry<String, String> entry : searchfull.entrySet()) {
            maps.add(entry);
        }

        for (LinkedHashMap<String, String> map : getMaps(TYPE.HUMAN)) {
            for (Entry<String, String> entry : map.entrySet()) {
                maps.add(entry);
            }
        }

        for (LinkedHashMap<String, String> map : getMaps(TYPE.OSIS)) {
            for (Entry<String, String> entry : map.entrySet()) {
                maps.add(entry);
            }
        }

        if (book == null || "".equals(book)) {
            for (int i = 0; i < this.osiss.size() && i < limit; ++i) {
                osiss.put(humans.get(i), this.osiss.get(i));
            }
            return osiss;
        }

        for (Entry<String, String> entry : maps) {
            if (checkStartSuggest(osiss, entry.getValue(), entry.getValue(), book, limit)) {
                return osiss;
            }
        }

        for (Entry<String, String> entry : maps) {
            if (checkContainSuggest(osiss, entry.getValue(), entry.getValue(), book, limit)) {
                return osiss;
            }
        }

        for (Entry<String, String> entry : maps) {
            if (checkStartSuggest(osiss, entry.getKey(), entry.getValue(), book, limit)) {
                return osiss;
            }
        }

        for (Entry<String, String> entry : maps) {
            if (checkContainSuggest(osiss, entry.getKey(), entry.getValue(), book, limit)) {
                return osiss;
            }
        }

        String osis = "";
        String chapter = "";
        if (osiss.size() == 0) {
            ArrayList<OsisItem> items = OsisItem.parseSearch(book, mContext);
            if (items.size() == 1) {
                OsisItem item = items.get(0);
                osis = item.book;
                chapter = item.chapter;
            }
        } else if (osiss.size() == 1) {
            for (Entry<String, String> entry : osiss.entrySet()) {
                osis = entry.getValue();
                chapter = "0";
            }
        }

        if ("".equals(osis)) {
            return osiss;
        }

        String bookname = get(TYPE.HUMAN, bible.getPosition(TYPE.OSIS, osis));
        if (bookname == null) {
            bookname = osis;
        }
        int chapternum = 0;
        int maxchapter = 0;
        try {
            chapternum = Integer.parseInt(chapter);
        } catch (Exception e) {
        }
        try {
            maxchapter = Integer.parseInt(get(TYPE.CHAPTER, getPosition(TYPE.OSIS, osis)));
        } catch (Exception e) {
            return osiss;
        }
        if (bookname == null || "".equals(bookname)) {
            return osiss;
        }
        if (chapternum != 0) {
            osiss.put(bookname + " " + chapternum, osis + chapternum);
        }
        for (int i = 0 + chapternum * 10; i <= maxchapter && i < 10 * chapternum + 10; i++) {
            if (i != 0) {
                osiss.put(bookname + " " + i, osis + i);
            }
        }

        return osiss;
    }

    public static boolean isCJK(String s) {
        for (char c : s.toCharArray()) {
            if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) {
                return true;
            }
        }
        return false;
    }

    public static Object getField(Object object, final Class<?> clazz, final String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(object);
        } catch (Exception e) {
            Log.e(TAG, "no such filed " + object.getClass().getName() + "." + fieldName);
        }
        return null;
    }

    public static Object getField(Object object, final String fieldName) {
        if (object == null) {
            return null;
        }
        return getField(object, object.getClass(), fieldName);
    }

    public boolean deleteVersion(String version) {
        boolean returncode = false;
        version = version.toLowerCase(Locale.US);
        File file = getFile(version);
        if (file != null && file.isFile() && file.delete()) {
            returncode = true;
        }
        synchronized (versionsLock) {
            Iterator<String> it = versions.iterator();
            while (it.hasNext()) {
                if (version.equals(it.next())) {
                    it.remove();
                }
            }
            versionpaths.remove(version);
        }
        checkBibleData(false, null);
        if (version.equalsIgnoreCase(databaseVersion)) {
            setVersion(get(TYPE.VERSION, 0));
        }
        return returncode;
    }

    @SuppressLint("NewApi")
    private void checkBibleData(boolean all) {
        synchronized (versionsCheckingLock) {
            if ((!checking || !all) && versions.size() > 0) {
                Log.d(TAG, "cancel checking");
                return;
            }
            if (!all) {
                if (checkVersionsSync(false) > 0) {
                    return;
                }
            }
            checkApkData();
            checkZipData(Environment.getExternalStorageDirectory());
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ECLAIR_MR1) {
                checkZipData(new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS));
            } else {
                checkZipData(new File(Environment.getExternalStorageDirectory(), "Download"));
            }
            checkVersionsSync(true);
        }
    }

    private volatile boolean checking = false;
    private volatile boolean checked = false;

    public void checkBibleData(boolean block, final Runnable run) {
        checking = true;
        if (block && !checked) {
            checkBibleData(true);
            if (run != null) {
                run.run();
            }
            checking = false;
            checked = true;
        } else {
            new Thread(new Runnable() {
                public void run() {
                    checkBibleData(true);
                    if (run != null) {
                        run.run();
                    }
                    checking = false;
                    checked = true;
                }
            }).start();
        }
    }

    private boolean checkZipData(File path) {
        if (path == null || !path.isDirectory() || path.list() == null) {
            return false;
        }
        Log.d(TAG, "checking zipdata " + path.getAbsolutePath());
        for (String name : path.list()) {
            if (name.startsWith("bibledata-") && name.endsWith("zip")) {
                try {
                    unpackZip(new File(path, name));
                } catch (IOException e) {
                } catch (Exception e) {
                    Log.e(TAG, "unpackZip", e);
                }
            }
        }
        return true;
    }

    private boolean unpackZip(File path) throws IOException {
        if (path == null || !path.isFile()) {
            return false;
        }

        File dirpath = getExternalFilesDirWrapper();

        // bibledata-zh-cn-version.zip
        String filename = path.getAbsolutePath();
        int sep = filename.lastIndexOf("-");
        if (sep != -1) {
            filename = filename.substring(sep + 1, filename.length() - 4);
        }
        filename += ".sqlite3";

        InputStream is = new FileInputStream(path);
        long fileSize = path.length();
        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
        try {
            ZipEntry ze;
            while ((ze = zis.getNextEntry()) != null) {
                long zeSize = ze.getCompressedSize();
                // zip is incomplete
                if (fileSize < zeSize) {
                    break;
                }
                String zename = ze.getName();
                if (zename == null || !zename.endsWith((".sqlite3"))) {
                    continue;
                }
                sep = zename.lastIndexOf(File.separator);
                if (sep != -1) {
                    zename = zename.substring(sep + 1);
                }
                File file;
                String version = zename.toLowerCase(Locale.US).replace(".sqlite3", "");
                if (versionpaths.containsKey(version)) {
                    file = new File(versionpaths.get(version));
                } else {
                    file = new File(dirpath, zename);
                }
                if (file.exists() && file.lastModified() > ze.getTime()
                        && file.lastModified() > path.lastModified()) {
                    continue;
                }
                Log.d(TAG, "unpacking " + file.getAbsoluteFile());
                int length;
                File tmpfile = new File(dirpath, zename + ".tmp");
                OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpfile));
                byte[] buffer = new byte[8192];
                int zero = 0;
                while ((length = zis.read(buffer)) != -1) {
                    if (length == 0) {
                        ++zero;
                        if (zero > 3) {
                            break;
                        }
                    } else {
                        zero = 0;
                    }
                    os.write(buffer, 0, length);
                }
                os.close();
                if (zero > 3) {
                    return false;
                } else {
                    tmpfile.renameTo(file);
                    path.delete();
                    return true;
                }
            }
        } finally {
            is.close();
            zis.close();
        }
        return false;
    }

    private void checkApkData() {
        Log.d(TAG, "checking apkdata");
        try {
            String packageName = mContext.getPackageName();
            PackageManager pm = mContext.getPackageManager();
            SharedPreferences preference = PreferenceManager.getDefaultSharedPreferences(mContext);
            ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
            for (String applicationName : pm.getPackagesForUid(ai.uid)) {
                if (packageName.equals(applicationName)) {
                    continue;
                }

                // version
                String version = applicationName.replace(packageName + ".", "");

                // resources
                Resources resources = mContext
                        .createPackageContext(applicationName, Context.CONTEXT_IGNORE_SECURITY).getResources();

                // newVersion
                int versionCode = pm.getPackageInfo(applicationName, 0).versionCode;
                boolean newVersion = (preference.getInt(version, 0) != versionCode);

                // resid
                int resid = resources.getIdentifier("a", "raw", applicationName);
                if (resid == 0) {
                    resid = resources.getIdentifier("xa", "raw", applicationName);
                }
                if (resid == 0) {
                    Log.d(TAG, "package " + applicationName + " has no R.raw.a nor R.raw.xa");
                    continue;
                }

                // file
                File file;
                if (versionpaths.containsKey(version)) {
                    file = new File(versionpaths.get(version));
                } else {
                    file = new File(getExternalFilesDirWrapper(), version + ".sqlite3");
                }
                if (file.exists() && !file.isFile()) {
                    file.delete();
                }

                boolean unpack = unpackRaw(resources, newVersion, resid, file);
                if (newVersion && unpack) {
                    preference.edit().putInt(version, versionCode).commit();
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "", e);
        }
    }

    private boolean unpackRaw(Resources resources, boolean newVersion, int resid, File file) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return false;
        }

        if (file.exists()) {
            if (!newVersion) {
                return true;
            }
            file.delete();
        }

        Log.d(TAG, "unpacking " + file.getAbsolutePath());

        try {
            int length;
            byte[] buffer = new byte[8192];
            File tmpfile = new File(file.getAbsolutePath() + ".tmp");
            OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpfile));
            for (int i = 0; i < 20; i++) {
                InputStream is = resources.openRawResource(resid + i);
                while ((length = is.read(buffer)) >= 0) {
                    os.write(buffer, 0, length);
                }
                is.close();
            }
            os.close();
            tmpfile.renameTo(file);
        } catch (Exception e) {
            Log.e(TAG, "unpacked " + file.getAbsolutePath(), e);
            return false;
        }

        return true;
    }

    int checkVersion(File path, List<String> versions, Map<String, String> versionpaths, boolean all) {
        if (!path.exists() || !path.isDirectory() || path.list() == null) {
            return versions.size();
        }
        String[] names = path.list();
        for (String name : names) {
            if (!all && versions.size() > 0) {
                break;
            }
            File file = new File(path, name);
            if (name.endsWith(".sqlite3") && file.exists() && file.isFile()) {
                Log.d(TAG, "add version " + name);
                String version = name.toLowerCase(Locale.US).replace(".sqlite3", "");
                if (!checkVersionMeta(file, version)) {
                    continue;
                }
                if (!versions.contains(version)) {
                    versions.add(version);
                }
                versionpaths.put(version, file.getAbsolutePath());
                if (version.equalsIgnoreCase("niv2011")) {
                    versionpaths.put("niv", file.getAbsolutePath());
                } else if (version.equalsIgnoreCase("niv1984")) {
                    versionpaths.put("niv84", file.getAbsolutePath());
                }
            }
        }
        return versions.size();
    }

    @SuppressLint("NewApi")
    private boolean isDatabaseIntegrityOk(SQLiteDatabase database) {
        // assume ok if the api is not available
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            return true;
        } else {
            return database.isDatabaseIntegrityOk();
        }
    }

    private boolean checkVersionMeta(File file, String version) {
        SQLiteDatabase metadata = null;
        try {
            metadata = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null,
                    SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
            String dataversion = version.replace("demo", "");
            if (!versionFullnames.containsKey(version)) {
                versionFullnames.put(version, getVersionMetadata("fullname", metadata, dataversion));
            }
            if (!versionNames.containsKey(version)) {
                versionNames.put(version, getVersionMetadata("name", metadata, dataversion));
            }
            versionDates.put(version, getVersionMetadata("date", metadata, "0"));
            // setMetadata(metadata, dataversion, false);
            return true;
        } catch (Exception e) {
            try {
                file.delete();
            } catch (Exception f) {
            }
            return false;
        } finally {
            if (metadata != null) {
                metadata.close();
            }
        }
    }

    public static final String BIBLEDATA_PREFIX = "http://github.com/liudongmiao/bibledata/raw/master/";

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public DownloadInfo download(String filename) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
            return null;
        }
        if (getExternalFilesDirWrapper() == null) {
            return null;
        }
        String url = BIBLEDATA_PREFIX + filename;
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        request.setTitle(filename);
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
        DownloadManager dm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        return DownloadInfo.getDownloadInfo(mContext, dm.enqueue(request));
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public void cancel(long id) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
            return;
        }
        DownloadManager dm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        dm.remove(id);
    }

    public Context getContext() {
        return mContext;
    }

    public static final String JSON = "versions.json";

    String getLocalVersions() throws IOException {
        InputStream is = null;
        File file = new File(mContext.getFilesDir(), JSON);
        if (file.isFile()) {
            is = new FileInputStream(file);
        } else {
            is = mContext.getResources().openRawResource(R.raw.versions);
        }
        String json = getStringFromInputStream(is);
        is.close();
        return json;
    }

    String getRemoteVersions() throws ClientProtocolException, IOException {
        HttpClient client = new DefaultHttpClient();
        SharedPreferences sp = mContext.getSharedPreferences("json", Context.MODE_MULTI_PROCESS);
        String etag = sp.getString(JSON + "_etag", null);
        HttpGet get = new HttpGet(Bible.BIBLEDATA_PREFIX + JSON);
        if (etag != null) {
            get.addHeader("If-None-Match", etag);
        }
        HttpResponse response = client.execute(get);
        if (response.getStatusLine().getStatusCode() == 304) {
            Log.d(TAG, JSON + " not modified");
            return null;
        }
        InputStream is = response.getEntity().getContent();
        String json = getStringFromInputStream(is);
        is.close();
        try {
            new JSONObject(json);
        } catch (JSONException e) {
            Log.d(TAG, "json: " + json);
            return null;
        }
        try {
            Header header = response.getFirstHeader("ETag");
            if (header != null) {
                sp.edit().putString(JSON + "_etag", header.getValue()).commit();
            }
            File tmpfile = new File(mContext.getFilesDir(), JSON + ".tmp");
            OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpfile));
            os.write(json.getBytes("UTF-8"));
            os.close();
            tmpfile.renameTo(new File(mContext.getFilesDir(), JSON));
        } catch (IOException e) {
        }
        return json;
    }

    String getStringFromInputStream(InputStream is) throws IOException {
        int length;
        byte[] buffer = new byte[8192];
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        while ((length = is.read(buffer)) >= 0) {
            bao.write(buffer, 0, length);
        }
        return bao.toString();
    }

    public void email(Context context) {
        email(context, null);
    }

    public void email(Context context, String content) {
        StringBuffer subject = new StringBuffer();
        subject.append(context.getString(R.string.app_name));
        if (versionName != null) {
            subject.append(" ");
            subject.append(versionName);
        }
        subject.append("(Android ");
        subject.append(Locale.getDefault().toString());
        subject.append("-");
        subject.append(Build.VERSION.RELEASE);
        subject.append(")");
        Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:liudongmiao@gmail.com"));
        intent.putExtra(Intent.EXTRA_SUBJECT, subject.toString());
        if (content != null) {
            intent.putExtra(Intent.EXTRA_TEXT, content);
        }
        try {
            context.startActivity(intent);
        } catch (ActivityNotFoundException e) {
        }
    }

    public void loadAnnotations(String osis, boolean load) {
        annotations.clear();
        if (!load) {
            return;
        }
        Cursor cursor = null;
        try {
            cursor = database.query("annotations", new String[] { "link", "content" }, "osis = ?",
                    new String[] { osis }, null, null, null, null);
            while (cursor.moveToNext()) {
                String link = cursor.getString(0);
                String content = cursor.getString(1);
                annotations.put(link, content);
            }
        } catch (SQLiteException e) {
            // ignore
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    public String getAnnotation(String link) {
        return annotations.get(link);
    }

    private String osis = null;
    private HashMap<String, Note> notes = new HashMap<String, Note>();

    /**
     * load notes, called when osis is changed
     * @param osis book.chapter
     */
    public void loadNotes(String osis) {
        if (osis == null) {
            return;
        }
        if (this.osis != null && this.osis.equals(osis)) {
            return;
        }
        this.osis = osis;
        notes.clear();
        if (mOpenHelper == null) {
            mOpenHelper = new AnnotationsDatabaseHelper(mContext);
        }
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        if (!isDatabaseIntegrityOk(db)) {
            return;
        }

        Cursor cursor = null;
        try {
            cursor = db.query(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, null, "osis = ? and type = ?",
                    new String[] { osis, "note" }, null, null, null);
            while (cursor != null && cursor.moveToNext()) {
                long id = cursor.getInt(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_ID));
                String verse = cursor.getString(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_VERSE));
                String verses = cursor.getString(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_VERSES));
                String content = cursor.getString(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_CONTENT));
                Long create = cursor.getLong(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_CONTENT));
                Long update = cursor.getLong(cursor.getColumnIndex(AnnotationsDatabaseHelper.COLUMN_UPDATETIME));
                notes.put(verse, new Note(id, verse, verses, content, create, update));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    class Note {
        Long id = null;
        String verse;
        String verses;
        String content;
        long createtime;
        long updatetime;
        ContentValues values = null;

        public Note(Long id, String verse, String verses, String content, long create, long update) {
            this.id = id;
            this.verse = verse;
            this.verses = verses;
            this.content = content;
            this.createtime = create;
            this.updatetime = update;
        }

        public Note(String verse, String verses, String content) {
            long time = System.currentTimeMillis() / 1000;
            this.verse = verse;
            this.verses = verses;
            this.content = content;
            this.createtime = time;
            this.updatetime = time;
        }

        public boolean update(String verses, String content) {
            if (this.verses.equals(verses) && this.content.equals(content)) {
                return false;
            }
            long time = System.currentTimeMillis() / 1000;
            this.verses = verses;
            this.content = content;
            this.updatetime = time;
            if (values != null) {
                values.put(AnnotationsDatabaseHelper.COLUMN_VERSES, verses);
                values.put(AnnotationsDatabaseHelper.COLUMN_CONTENT, content);
                values.put(AnnotationsDatabaseHelper.COLUMN_UPDATETIME, time);
            }
            return true;
        }

        public ContentValues getContentValues() {
            if (values == null) {
                values = new ContentValues();
                values.put(AnnotationsDatabaseHelper.COLUMN_TYPE, "note");
                values.put(AnnotationsDatabaseHelper.COLUMN_VERSE, verse);
                values.put(AnnotationsDatabaseHelper.COLUMN_VERSES, verses);
                values.put(AnnotationsDatabaseHelper.COLUMN_CONTENT, content);
                values.put(AnnotationsDatabaseHelper.COLUMN_CREATETIME, createtime);
                values.put(AnnotationsDatabaseHelper.COLUMN_UPDATETIME, updatetime);
            }
            return values;
        }

        public Long getId() {
            return this.id;
        }

        public void setId(Long id) {
            this.id = id;
        }
    }

    /**
     * get a list of verse for osis, called when the reading page refresh.
     * @param osis book.chapter
     * @return
     */
    public String[] getNoteVerses(String osis) {
        if (osis == null) {
            return null;
        }
        if (!osis.equals(this.osis)) {
            loadNotes(osis);
        }
        return notes.keySet().toArray(new String[0]);
    }

    /**
     * get notes for book.cpater.verse
     * @param osis book.chapter
     * @param verse the verse is one of the result in {@link #getNoteVerses(String)}
     * @return note
     * @see {@link #getNoteVerses(String)}
     */
    public Note getNote(String osis, String verse) {
        if (osis == null) {
            return null;
        }
        if (!osis.equals(this.osis)) {
            loadNotes(osis);
        }
        return notes.get(verse);
    }

    /**
     * save the note for book.chapter.verse
     * @param osis book.chapter
     * @param verse verse, normally number
     * @param content note content
     * @return true if saved, false if not
     */
    public boolean saveNote(String osis, String verse, String verses, String content) {
        if (content == null || content.length() == 0) {
            return false;
        }
        Note note = notes.get(verse);
        if (note == null) {
            note = new Note(verse, verses, content);
        } else if (!note.update(verses, content)) {
            return false;
        }
        notes.put(verse, note);
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        if (!isDatabaseIntegrityOk(db)) {
            File path = mContext.getDatabasePath(AnnotationsDatabaseHelper.DATABASE_NAME);
            if (path != null && path.delete()) {
                return saveNote(osis, verse, verses, content);
            }
            return false;
        }
        ContentValues values = note.getContentValues();
        values.put(AnnotationsDatabaseHelper.COLUMN_OSIS, osis);
        Long id = note.getId();
        if (id == null) {
            id = db.insert(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, null, values);
            note.setId(id);
        } else {
            db.update(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, values,
                    AnnotationsDatabaseHelper.COLUMN_ID + " = ?", new String[] { String.valueOf(id) });
        }
        return true;
    }

    public boolean deleteNote(Note note) {
        if (note == null || note.getId() == null) {
            return false;
        }
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        if (!isDatabaseIntegrityOk(db)) {
            return true;
        }
        db.delete(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, AnnotationsDatabaseHelper.COLUMN_ID + " = ?",
                new String[] { String.valueOf(note.getId()) });
        notes.remove(note.verse);
        return true;
    }

    Long highlightId = null;
    String highlighted = "";

    public String getHighlight(String osis) {
        highlightId = null;
        highlighted = "";
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        if (!isDatabaseIntegrityOk(db)) {
            return highlighted;
        }
        Cursor cursor = null;
        try {
            cursor = db.query(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS,
                    new String[] { AnnotationsDatabaseHelper.COLUMN_ID, AnnotationsDatabaseHelper.COLUMN_VERSES },
                    AnnotationsDatabaseHelper.COLUMN_OSIS + " = ? and " + AnnotationsDatabaseHelper.COLUMN_TYPE
                            + " = ?",
                    new String[] { osis, "highlight" }, null, null, null);
            while (cursor != null && cursor.moveToNext()) {
                highlightId = cursor.getLong(0);
                highlighted = cursor.getString(1);
            }
        } catch (SQLiteException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return highlighted;
    }

    public boolean saveHighlight(String osis, String verses) {
        if (highlighted.equals(verses)) {
            return false;
        }
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        if (!isDatabaseIntegrityOk(db)) {
            File path = mContext.getDatabasePath(AnnotationsDatabaseHelper.DATABASE_NAME);
            if (path != null && path.delete()) {
                return saveHighlight(osis, verses);
            }
            return false;
        }
        ContentValues values = new ContentValues();
        values.put(AnnotationsDatabaseHelper.COLUMN_OSIS, osis);
        values.put(AnnotationsDatabaseHelper.COLUMN_TYPE, "highlight");
        values.put(AnnotationsDatabaseHelper.COLUMN_VERSES, verses);
        if (highlightId == null) {
            highlightId = db.insert(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, null, values);
        } else {
            db.update(AnnotationsDatabaseHelper.TABLE_ANNOTATIONS, values,
                    AnnotationsDatabaseHelper.COLUMN_ID + " = ?", new String[] { String.valueOf(highlightId) });
        }
        return true;
    }

    private SQLiteOpenHelper mOpenHelper = null;

    private class AnnotationsDatabaseHelper extends SQLiteOpenHelper {

        private static final int DATABASE_VERSION = 2;
        private static final String DATABASE_NAME = "annotations.db";

        AnnotationsDatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        public static final String TABLE_ANNOTATIONS = "annotations";
        public static final String COLUMN_ID = BaseColumns._ID;
        public static final String COLUMN_TYPE = "type";
        public static final String COLUMN_OSIS = "osis";
        public static final String COLUMN_VERSE = "verse";
        public static final String COLUMN_VERSES = "verses";
        public static final String COLUMN_CONTENT = "content";
        public static final String COLUMN_CREATETIME = "createtime";
        public static final String COLUMN_UPDATETIME = "updatetime";

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("DROP TABLE IF EXISTS " + TABLE_ANNOTATIONS);
            db.execSQL("CREATE TABLE " + TABLE_ANNOTATIONS + " (" + COLUMN_ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT," + COLUMN_OSIS + " VARCHAR NOT NULL," + COLUMN_TYPE
                    + " VARCHAR NOT NULL," + COLUMN_VERSE + " VARCHAR," + COLUMN_VERSES + " TEXT," + COLUMN_CONTENT
                    + " TEXT," + COLUMN_CREATETIME + " DATETIME," + COLUMN_UPDATETIME + " DATETIME" + ");");

            db.execSQL("CREATE INDEX osis_tye ON " + TABLE_ANNOTATIONS + " (" + COLUMN_OSIS + ", " + COLUMN_TYPE
                    + ");");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            if (oldVersion < 2) {
                onCreate(db);
            }
        }

    }
}