diff --git a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java
index b979a67e27..853dfc7c4b 100644
--- a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java
+++ b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java
@@ -282,8 +282,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback.
+
+ Copyright 2018-2022 by Marcel Bokhorst (M66B)
+*/
+
+import android.annotation.SuppressLint;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.mail.Address;
+
+import io.requery.android.database.sqlite.SQLiteDatabase;
+import io.requery.android.database.sqlite.SQLiteOpenHelper;
+
+// https://www.sqlite.org/fts3.html
+// fts4 requires sqlite version 3.7.4, API 21 Android
+public class Fts4DbHelper extends SQLiteOpenHelper {
+ private Context context;
+
+ @SuppressLint("StaticFieldLeak")
+ private static Fts4DbHelper instance = null;
+
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "fts4.db";
+
+ private Fts4DbHelper(Context context) {
+ super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
+ this.context = context.getApplicationContext();
+ }
+
+ static SQLiteDatabase getInstance(Context context) {
+ if (instance == null) {
+ if (!context.getDatabasePath(DATABASE_NAME).exists()) {
+ Fts5DbHelper.delete(context);
+ DB.getInstance(context).message().resetFts();
+ }
+ instance = new Fts4DbHelper(context);
+ }
+ return instance.getWritableDatabase();
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.i("FTS create");
+ db.execSQL("CREATE VIRTUAL TABLE `message`" +
+ " USING fts4" +
+ " (`account`" +
+ ", `folder`" +
+ ", `time`" +
+ ", `address`" +
+ ", `subject`" +
+ ", `keyword`" +
+ ", `text`" +
+ ", `notes`" +
+ ", notindexed=`account`" +
+ ", notindexed=`folder`" +
+ ", notindexed=`time`" +
+ ", tokenize=unicode61 \"remove_diacritics=2\")");
+ // https://www.sqlite.org/fts3.html#tokenizer
+ // https://unicode.org/reports/tr29/
+
+ // https://www.sqlite.org/fts3.html#fts4aux
+ db.execSQL("CREATE VIRTUAL TABLE message_terms USING fts4aux('message');");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.i("FTS upgrade from " + oldVersion + " to " + newVersion);
+
+ db.execSQL("DROP TABLE IF EXISTS `message`");
+ db.execSQL("DROP TABLE IF EXISTS `message_terms`");
+
+ onCreate(db);
+
+ DB.getInstance(context).message().resetFts();
+ }
+
+ static void insert(SQLiteDatabase db, EntityMessage message, String text) {
+ Log.i("FTS insert id=" + message.id);
+ List
address = new ArrayList<>();
+ if (message.from != null)
+ address.addAll(Arrays.asList(message.from));
+ if (message.to != null)
+ address.addAll(Arrays.asList(message.to));
+ if (message.cc != null)
+ address.addAll(Arrays.asList(message.cc));
+ if (message.bcc != null)
+ address.addAll(Arrays.asList(message.bcc));
+
+ delete(db, message.id);
+
+ ContentValues cv = new ContentValues();
+ cv.put("rowid", message.id);
+ cv.put("account", message.account);
+ cv.put("folder", message.folder);
+ cv.put("time", message.received);
+ cv.put("address", MessageHelper.formatAddresses(address.toArray(new Address[0]), true, false));
+ cv.put("subject", message.subject == null ? "" : message.subject);
+ cv.put("keyword", TextUtils.join(", ", message.keywords));
+ cv.put("text", text);
+ cv.put("notes", message.notes);
+ db.insert("message", SQLiteDatabase.CONFLICT_FAIL, cv);
+ }
+
+ static void delete(SQLiteDatabase db) {
+ db.delete("message", null, null);
+ }
+
+ static void delete(SQLiteDatabase db, long id) {
+ db.delete("message", "rowid = ?", new Object[]{id});
+ }
+
+ static List getSuggestions(SQLiteDatabase db, String query, int max) {
+ List result = new ArrayList<>();
+
+ try (Cursor cursor = db.query(
+ "SELECT term FROM message_terms" +
+ " WHERE term LIKE ?" +
+ " ORDER BY occurrences DESC" +
+ " LIMIT " + max,
+ new Object[]{query})) {
+ while (cursor != null && cursor.moveToNext())
+ result.add(cursor.getString(0));
+ }
+
+ return result;
+ }
+
+ static List match(
+ SQLiteDatabase db,
+ Long account, Long folder, long[] exclude,
+ BoundaryCallbackMessages.SearchCriteria criteria) {
+
+ List word = new ArrayList<>();
+ List plus = new ArrayList<>();
+ List minus = new ArrayList<>();
+ List opt = new ArrayList<>();
+ StringBuilder all = new StringBuilder();
+ for (String w : criteria.query.trim().split("\\s+")) {
+ if (all.length() > 0)
+ all.append(' ');
+
+ if (w.length() > 1 && w.startsWith("+")) {
+ plus.add(w.substring(1));
+ all.append(w.substring(1));
+ } else if (w.length() > 1 && w.startsWith("-")) {
+ minus.add(w.substring(1));
+ all.append(w.substring(1));
+ } else if (w.length() > 1 && w.startsWith("?")) {
+ opt.add(w.substring(1));
+ all.append(w.substring(1));
+ } else {
+ word.add(w);
+ all.append(w);
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (plus.size() + minus.size() + opt.size() > 0) {
+ if (word.size() > 0)
+ sb.append(escape(TextUtils.join(" ", word)));
+
+ for (String p : plus) {
+ if (sb.length() > 0)
+ sb.append(" AND ");
+ sb.append(escape(p));
+ }
+
+ for (String m : minus) {
+ if (sb.length() > 0)
+ sb.append(" NOT ");
+ sb.append(escape(m));
+ }
+
+ if (sb.length() > 0) {
+ sb.insert(0, '(');
+ sb.append(')');
+ }
+
+ for (String o : opt) {
+ if (sb.length() > 0)
+ sb.append(" OR ");
+ sb.append(escape(o));
+ }
+ }
+
+ String search = (sb.length() > 0 ? sb.toString() : escape(criteria.query));
+
+ String select = "";
+ if (account != null)
+ select += "account = " + account + " AND ";
+ if (folder != null)
+ select += "folder = " + folder + " AND ";
+ if (exclude.length > 0) {
+ select += "NOT folder IN (";
+ for (int i = 0; i < exclude.length; i++) {
+ if (i > 0)
+ select += ", ";
+ select += exclude[i];
+ }
+ select += ") AND ";
+ }
+ if (criteria.after != null)
+ select += "time > " + criteria.after + " AND ";
+ if (criteria.before != null)
+ select += "time < " + criteria.before + " AND ";
+
+ Log.i("FTS select=" + select + " search=" + search);
+ List result = new ArrayList<>();
+ try (Cursor cursor = db.query(
+ "message", new String[]{"rowid"},
+ select + "message MATCH ?",
+ new Object[]{search},
+ null, null, "time DESC", null)) {
+ while (cursor != null && cursor.moveToNext())
+ result.add(cursor.getLong(0));
+ }
+ Log.i("FTS result=" + result.size());
+ return result;
+ }
+
+ private static String escape(String word) {
+ return "\"" + word.replaceAll("\"", "\"\"") + "\"";
+ }
+
+ static Cursor getIds(SQLiteDatabase db) {
+ return db.query(
+ "message", new String[]{"rowid"},
+ null, null,
+ null, null, "time");
+ }
+
+ static long size(Context context) {
+ return context.getDatabasePath(DATABASE_NAME).length();
+ }
+
+ static void optimize(SQLiteDatabase db) {
+ Log.i("FTS optimize");
+ db.execSQL("INSERT INTO message (message) VALUES ('optimize')");
+ }
+
+ static void delete(Context context) {
+ context.getDatabasePath(DATABASE_NAME).delete();
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/FtsDbHelper.java b/app/src/main/java/eu/faircode/email/Fts5DbHelper.java
similarity index 97%
rename from app/src/main/java/eu/faircode/email/FtsDbHelper.java
rename to app/src/main/java/eu/faircode/email/Fts5DbHelper.java
index 9417ff15b3..16f8b76c50 100644
--- a/app/src/main/java/eu/faircode/email/FtsDbHelper.java
+++ b/app/src/main/java/eu/faircode/email/Fts5DbHelper.java
@@ -35,23 +35,23 @@ import io.requery.android.database.sqlite.SQLiteDatabase;
import io.requery.android.database.sqlite.SQLiteOpenHelper;
// https://www.sqlite.org/fts5.html
-public class FtsDbHelper extends SQLiteOpenHelper {
+public class Fts5DbHelper extends SQLiteOpenHelper {
private Context context;
@SuppressLint("StaticFieldLeak")
- private static FtsDbHelper instance = null;
+ private static Fts5DbHelper instance = null;
private static final int DATABASE_VERSION = 5;
private static final String DATABASE_NAME = "fts.db";
- private FtsDbHelper(Context context) {
+ private Fts5DbHelper(Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
this.context = context.getApplicationContext();
}
static SQLiteDatabase getInstance(Context context) {
if (instance == null)
- instance = new FtsDbHelper(context);
+ instance = new Fts5DbHelper(context);
return instance.getWritableDatabase();
}
diff --git a/app/src/main/java/eu/faircode/email/WorkerCleanup.java b/app/src/main/java/eu/faircode/email/WorkerCleanup.java
index bcb6c0bc7f..4deb2b5f9f 100644
--- a/app/src/main/java/eu/faircode/email/WorkerCleanup.java
+++ b/app/src/main/java/eu/faircode/email/WorkerCleanup.java
@@ -334,21 +334,21 @@ public class WorkerCleanup extends Worker {
Log.i("Cleanup FTS=" + fts);
if (fts) {
int deleted = 0;
- SQLiteDatabase sdb = FtsDbHelper.getInstance(context);
- try (Cursor cursor = FtsDbHelper.getIds(sdb)) {
+ SQLiteDatabase sdb = Fts4DbHelper.getInstance(context);
+ try (Cursor cursor = Fts4DbHelper.getIds(sdb)) {
while (cursor.moveToNext()) {
long rowid = cursor.getLong(0);
EntityMessage message = db.message().getMessage(rowid);
if (message == null || !message.fts) {
Log.i("Deleting FTS rowid=" + rowid);
- FtsDbHelper.delete(sdb, rowid);
+ Fts4DbHelper.delete(sdb, rowid);
deleted++;
}
}
}
Log.i("Cleanup FTS=" + deleted);
if (manual)
- FtsDbHelper.optimize(sdb);
+ Fts4DbHelper.optimize(sdb);
}
Log.i("Cleanup contacts");
diff --git a/app/src/main/java/eu/faircode/email/WorkerFts.java b/app/src/main/java/eu/faircode/email/WorkerFts.java
index d2e6c193b0..3020841c89 100644
--- a/app/src/main/java/eu/faircode/email/WorkerFts.java
+++ b/app/src/main/java/eu/faircode/email/WorkerFts.java
@@ -65,7 +65,7 @@ public class WorkerFts extends Worker {
List ids = new ArrayList<>(INDEX_BATCH_SIZE);
DB db = DB.getInstance(context);
- SQLiteDatabase sdb = FtsDbHelper.getInstance(context);
+ SQLiteDatabase sdb = Fts4DbHelper.getInstance(context);
try (Cursor cursor = db.message().getMessageFts()) {
while (cursor != null && cursor.moveToNext())
@@ -92,7 +92,7 @@ public class WorkerFts extends Worker {
try {
sdb.beginTransaction();
- FtsDbHelper.insert(sdb, message, text);
+ Fts4DbHelper.insert(sdb, message, text);
sdb.setTransactionSuccessful();
} finally {
sdb.endTransaction();