package com.quran.labs.androidquran.database;
import android.content.Context;
import android.database.Cursor;
import android.database.DefaultDatabaseErrorHandler;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.provider.BaseColumns;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import com.crashlytics.android.Crashlytics;
import com.quran.labs.androidquran.R;
import com.quran.labs.androidquran.common.QuranText;
import com.quran.labs.androidquran.data.VerseRange;
import com.quran.labs.androidquran.util.QuranFileUtils;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import timber.log.Timber;
public class DatabaseHandler {
private static final String COL_SURA = "sura";
private static final String COL_AYAH = "ayah";
private static final String COL_TEXT = "text";
public static final String VERSE_TABLE = "verses";
public static final String ARABIC_TEXT_TABLE = "arabic_text";
private static final String PROPERTIES_TABLE = "properties";
private static final String COL_PROPERTY = "property";
private static final String COL_VALUE = "value";
private static final String MATCH_END = "</font>";
private static final String ELLIPSES = "<b>...</b>";
private static Map<String, DatabaseHandler> databaseMap = new HashMap<>();
private int schemaVersion = 1;
private String matchString;
private SQLiteDatabase database = null;
@Retention(RetentionPolicy.SOURCE)
@IntDef( { TextType.ARABIC, TextType.TRANSLATION } )
public @interface TextType {
int ARABIC = 0;
int TRANSLATION = 1;
}
public static synchronized DatabaseHandler getDatabaseHandler(
Context context, String databaseName) {
DatabaseHandler handler = databaseMap.get(databaseName);
if (handler == null) {
handler = new DatabaseHandler(context.getApplicationContext(), databaseName);
databaseMap.put(databaseName, handler);
}
return handler;
}
private DatabaseHandler(Context context, String databaseName) throws SQLException {
String base = QuranFileUtils.getQuranDatabaseDirectory(context);
if (base == null) return;
String path = base + File.separator + databaseName;
Crashlytics.log("opening database file: " + path);
try {
database = SQLiteDatabase.openDatabase(path, null,
SQLiteDatabase.NO_LOCALIZED_COLLATORS, new DefaultDatabaseErrorHandler());
} catch (SQLiteDatabaseCorruptException sce) {
Crashlytics.log("corrupt database: " + databaseName);
throw sce;
} catch (SQLException se){
Crashlytics.log("database file " + path +
(new File(path).exists()? " exists" : " doesn't exist"));
throw se;
}
schemaVersion = getSchemaVersion();
matchString = "<font color=\"" +
ContextCompat.getColor(context, R.color.translation_highlight) +
"\">";
}
public boolean validDatabase() {
return database != null && database.isOpen();
}
private Cursor getVerses(int sura, int minAyah, int maxAyah) {
return getVerses(sura, minAyah, maxAyah, VERSE_TABLE);
}
private int getProperty(@NonNull String column) {
int value = 1;
if (!validDatabase()) {
return value;
}
Cursor cursor = null;
try {
cursor = database.query(PROPERTIES_TABLE, new String[]{ COL_VALUE },
COL_PROPERTY + "= ?", new String[]{ column }, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
value = cursor.getInt(0);
}
return value;
} catch (SQLException se) {
return value;
} finally {
DatabaseUtils.closeCursor(cursor);
}
}
private int getSchemaVersion() {
return getProperty("schema_version");
}
public int getTextVersion() {
return getProperty("text_version");
}
private Cursor getVerses(int sura, int minAyah, int maxAyah, String table) {
return getVerses(sura, minAyah, sura, maxAyah, table);
}
/**
* @deprecated use {@link #getVerses(VerseRange, int)} instead
*
* @param minSura start sura
* @param minAyah start ayah
* @param maxSura end sura
* @param maxAyah end ayah
* @param table the table
* @return a Cursor with the data
*/
public Cursor getVerses(int minSura, int minAyah, int maxSura,
int maxAyah, String table) {
return getVersesInternal(new VerseRange(minSura, minAyah, maxSura, maxAyah), table);
}
public List<QuranText> getVerses(VerseRange verses, @TextType int textType) {
Cursor cursor = null;
List<QuranText> results = new ArrayList<>();
try {
String table = textType == TextType.ARABIC ? ARABIC_TEXT_TABLE : VERSE_TABLE;
cursor = getVersesInternal(verses, table);
while (cursor != null && cursor.moveToNext()) {
int sura = cursor.getInt(1);
int ayah = cursor.getInt(2);
String text = cursor.getString(3);
results.add(new QuranText(sura, ayah, text));
}
} finally {
DatabaseUtils.closeCursor(cursor);
}
return results;
}
private Cursor getVersesInternal(VerseRange verses, String table) {
if (!validDatabase()) {
return null;
}
StringBuilder whereQuery = new StringBuilder();
whereQuery.append("(");
if (verses.startSura == verses.endingSura) {
whereQuery.append(COL_SURA)
.append("=").append(verses.startSura)
.append(" and ").append(COL_AYAH)
.append(">=").append(verses.startAyah)
.append(" and ").append(COL_AYAH)
.append("<=").append(verses.endingAyah);
} else {
// (sura = minSura and ayah >= minAyah)
whereQuery.append("(").append(COL_SURA).append("=")
.append(verses.startSura).append(" and ")
.append(COL_AYAH).append(">=").append(verses.startAyah).append(")");
whereQuery.append(" or ");
// (sura = maxSura and ayah <= maxAyah)
whereQuery.append("(").append(COL_SURA).append("=")
.append(verses.endingSura).append(" and ")
.append(COL_AYAH).append("<=").append(verses.endingAyah).append(")");
whereQuery.append(" or ");
// (sura > minSura and sura < maxSura)
whereQuery.append("(").append(COL_SURA).append(">")
.append(verses.startSura).append(" and ")
.append(COL_SURA).append("<")
.append(verses.endingSura).append(")");
}
whereQuery.append(")");
return database.query(table,
new String[] { "rowid as _id", COL_SURA, COL_AYAH, COL_TEXT },
whereQuery.toString(), null, null, null,
COL_SURA + "," + COL_AYAH);
}
/**
* @deprecated use {@link #getVerses(VerseRange, int)} instead
* @param sura the sura
* @param ayah the ayah
* @return the result
*/
public Cursor getVerse(int sura, int ayah) {
return getVerses(sura, ayah, ayah);
}
public Cursor getVersesByIds(List<Integer> ids) {
StringBuilder builder = new StringBuilder();
for (int i = 0, idsSize = ids.size(); i < idsSize; i++) {
if (i > 0) {
builder.append(",");
}
builder.append(ids.get(i));
}
Timber.d("querying verses by ids for tags...");
final String sql = "SELECT rowid as _id, " + COL_SURA + ", " + COL_AYAH + ", " + COL_TEXT +
" FROM " + ARABIC_TEXT_TABLE + " WHERE rowid in(" + builder.toString() + ")";
return database.rawQuery(sql, null);
}
public Cursor search(String query, boolean withSnippets) {
return search(query, VERSE_TABLE, withSnippets);
}
public Cursor search(String q, String table, boolean withSnippets) {
if (!validDatabase()) {
return null;
}
final String limit = withSnippets ? "" : "LIMIT 25";
String query = q;
String operator = " like ";
String whatTextToSelect = COL_TEXT;
boolean useFullTextIndex = (schemaVersion > 1);
if (useFullTextIndex) {
operator = " MATCH ";
query = query + "*";
} else {
query = "%" + query + "%";
}
int pos = 0;
int found = 0;
boolean done = false;
while (!done) {
int quote = query.indexOf("\"", pos);
if (quote > -1) {
found++;
pos = quote + 1;
} else {
done = true;
}
}
if (found % 2 != 0) {
query = query.replaceAll("\"", "");
}
if (useFullTextIndex && withSnippets) {
whatTextToSelect = "snippet(" + table + ", '" +
matchString + "', '" + MATCH_END +
"', '" + ELLIPSES + "', -1, 64)";
}
String qtext = "select rowid as " + BaseColumns._ID + ", " + COL_SURA + ", " + COL_AYAH +
", " + whatTextToSelect + " from " + table + " where " + COL_TEXT +
operator + " ? " + " " + limit;
Crashlytics.log("search query: " + qtext + ", query: " + query);
try {
return database.rawQuery(qtext, new String[]{ query });
} catch (Exception e){
Crashlytics.logException(e);
return null;
}
}
}