/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * 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 org.kontalk.provider; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; import android.annotation.TargetApi; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.SQLException; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import org.kontalk.BuildConfig; import org.kontalk.Log; import org.kontalk.message.GroupCommandComponent; import org.kontalk.message.TextComponent; import org.kontalk.provider.MyMessages.CommonColumns; import org.kontalk.provider.MyMessages.Groups; import org.kontalk.provider.MyMessages.Messages; import org.kontalk.provider.MyMessages.Messages.Fulltext; import org.kontalk.provider.MyMessages.Threads; import org.kontalk.provider.MyMessages.Threads.Conversations; import org.kontalk.util.SystemUtils; /** * The message storage provider. * @author Daniele Ricci */ public class MessagesProvider extends ContentProvider { private static final String TAG = MessagesProvider.class.getSimpleName(); public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".messages"; private static final String TABLE_MESSAGES = "messages"; private static final String TABLE_MESSAGES_GROUPS = "messages_groups"; private static final String TABLE_FULLTEXT = "fulltext"; private static final String TABLE_THREADS = "threads"; private static final String TABLE_GROUPS = "groups"; private static final String TABLE_GROUP_MEMBERS = "group_members"; private static final String TABLE_THREADS_GROUPS = TABLE_THREADS + " LEFT OUTER JOIN " + TABLE_GROUPS + " ON " + TABLE_THREADS + "." + Threads._ID + "=" + TABLE_GROUPS + "." + Groups.THREAD_ID; private static final int THREADS = 1; private static final int THREADS_ID = 2; private static final int THREADS_PEER = 3; private static final int MESSAGES = 4; private static final int MESSAGES_ID = 5; private static final int MESSAGES_SERVERID = 6; private static final int CONVERSATIONS_ID = 7; private static final int CONVERSATIONS_ALL_ID = 8; private static final int GROUPS = 9; private static final int GROUPS_ID = 10; private static final int GROUPS_MEMBERS = 11; private static final int GROUPS_MEMBERS_ID = 12; private static final int FULLTEXT_ID = 13; private static final int REQUESTS = 14; private DatabaseHelper dbHelper; private static final UriMatcher sUriMatcher; private static HashMap<String, String> messagesProjectionMap; private static HashMap<String, String> threadsProjectionMap; private static HashMap<String, String> fulltextProjectionMap; private static HashMap<String, String> groupsMembersProjectionMap; private static HashMap<String, String> groupsProjectionMap; @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) static class DatabaseHelper extends SQLiteOpenHelper { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) static final int DATABASE_VERSION = 12; @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) static final String DATABASE_NAME = "messages.db"; private static final String _SCHEMA_MESSAGES = "(" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "thread_id INTEGER NOT NULL, " + "msg_id TEXT NOT NULL, " + // UNIQUE "peer TEXT NOT NULL, " + "direction INTEGER NOT NULL, " + "unread INTEGER NOT NULL DEFAULT 0, " + "new INTEGER NOT NULL DEFAULT 0, " + // this the sent/received timestamp // this will not change after insert EVER "timestamp INTEGER NOT NULL," + // this the timestamp of the latest status change // updated when status field is modified "status_changed INTEGER," + "status INTEGER," + // for text body or encrypted stanza "body_mime TEXT," + // if null couldn't be determined yet (e.g. encrypted message) "body_content BLOB," + // message body or encrypted e2e content (if mime is null) "body_length INTEGER NOT NULL DEFAULT 0," + // for a single attachment "att_mime TEXT, " + "att_preview_path TEXT," + "att_fetch_url TEXT," + "att_local_uri TEXT," + "att_length INTEGER NOT NULL DEFAULT 0," + "att_compress INTEGER NOT NULL DEFAULT 0," + "att_encrypted INTEGER NOT NULL DEFAULT 0," + "att_security_flags INTEGER NOT NULL DEFAULT 0," + // TODO geo_lat, geo_lon, ... // whole content encrypted "encrypted INTEGER NOT NULL DEFAULT 0, " + // security flags "security_flags INTEGER NOT NULL DEFAULT 0," + // timestamp declared by server for incoming messages // timestamp of message accepted by server for outgoing messages "server_timestamp INTEGER" + ")"; /** This table will contain all the messages .*/ private static final String SCHEMA_MESSAGES = "CREATE TABLE " + TABLE_MESSAGES + " " + _SCHEMA_MESSAGES; private static final String _SCHEMA_THREADS = "(" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "msg_id TEXT NOT NULL, " + // UNIQUE "peer TEXT NOT NULL UNIQUE, " + "direction INTEGER NOT NULL, " + "count INTEGER NOT NULL DEFAULT 0, " + "unread INTEGER NOT NULL DEFAULT 0, " + "new INTEGER NOT NULL DEFAULT 0, " + "mime TEXT, " + "content TEXT, " + // this the sent/received timestamp "timestamp INTEGER NOT NULL," + // this the timestamp of the latest status change "status_changed INTEGER," + "status INTEGER," + "encrypted INTEGER NOT NULL DEFAULT 0, " + "draft TEXT," + "request_status INTEGER NOT NULL DEFAULT 0," + "sticky INTEGER NOT NULL DEFAULT 0," + "encryption INTEGER NOT NULL DEFAULT 1" + ")"; /** This table will contain the latest message from each conversation. */ private static final String SCHEMA_THREADS = "CREATE TABLE " + TABLE_THREADS + " " + _SCHEMA_THREADS; private static final String _SCHEMA_GROUPS = "(" + "group_jid TEXT NOT NULL PRIMARY KEY, " + "thread_id INTEGER NOT NULL," + "group_type TEXT NOT NULL," + "subject TEXT," + "membership INTEGER NOT NULL DEFAULT 1" + ")"; /** This table will contain the groups definitions.*/ private static final String SCHEMA_GROUPS = "CREATE TABLE " + TABLE_GROUPS + " " + _SCHEMA_GROUPS; private static final String _SCHEMA_GROUP_MEMBERS = "(" + "group_jid TEXT NOT NULL, " + "group_peer TEXT NOT NULL, " + "pending INTEGER NOT NULL DEFAULT 0," + "PRIMARY KEY (group_jid, group_peer)" + ")"; /** This table will contain the groups participants .*/ private static final String SCHEMA_GROUPS_MEMBERS = "CREATE TABLE " + TABLE_GROUP_MEMBERS + " " + _SCHEMA_GROUP_MEMBERS; /** A view to link messages and groups. */ private static final String SCHEMA_MESSAGES_GROUPS = "CREATE VIEW " + TABLE_MESSAGES_GROUPS + " AS " + "SELECT " + TABLE_MESSAGES + ".*," + TABLE_GROUPS + "." + Groups.GROUP_JID + "," + TABLE_GROUPS + "." + Groups.SUBJECT + "," + TABLE_GROUPS + "." + Groups.GROUP_TYPE + "," + TABLE_GROUPS + "." + Groups.MEMBERSHIP + " FROM " + TABLE_MESSAGES + " LEFT JOIN " + TABLE_THREADS + " ON " + TABLE_MESSAGES + "." + Messages.THREAD_ID + "=" + TABLE_THREADS + "." + Threads._ID + " LEFT OUTER JOIN " + TABLE_GROUPS + " ON " + TABLE_THREADS + "." + Threads._ID + "=" + TABLE_GROUPS + "." + Groups.THREAD_ID; /** This table will contain every text message to speed-up full text searches. */ private static final String SCHEMA_FULLTEXT = "CREATE VIRTUAL TABLE " + TABLE_FULLTEXT + " USING fts3 (" + "thread_id INTEGER NOT NULL, " + "content TEXT" + ")"; private static final String SCHEMA_MESSAGES_INDEX = "CREATE UNIQUE INDEX IF NOT EXISTS unique_message ON " + TABLE_MESSAGES + " (msg_id, direction)"; private static final String SCHEMA_MESSAGES_TIMESTAMP_IDX = "CREATE INDEX IF NOT EXISTS timestamp_message ON " + TABLE_MESSAGES + " (timestamp)"; /** Updates the thread messages count. */ private static final String UPDATE_MESSAGES_COUNT_NEW = "UPDATE " + TABLE_THREADS + " SET count = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = new.thread_id" + ") WHERE _id = new.thread_id"; private static final String UPDATE_MESSAGES_COUNT_OLD = "UPDATE " + TABLE_THREADS + " SET count = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = old.thread_id" + ") WHERE _id = old.thread_id"; /** Updates the thread unread count. */ private static final String UPDATE_UNREAD_COUNT_NEW = "UPDATE " + TABLE_THREADS + " SET unread = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = new.thread_id " + "AND unread <> 0) WHERE _id = new.thread_id"; private static final String UPDATE_UNREAD_COUNT_OLD = "UPDATE " + TABLE_THREADS + " SET unread = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = old.thread_id " + "AND unread <> 0) WHERE _id = old.thread_id"; /** Updates the thread new count. */ private static final String UPDATE_NEW_COUNT_NEW = "UPDATE " + TABLE_THREADS + " SET \"new\" = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = new.thread_id " + "AND \"new\" <> 0) WHERE _id = new.thread_id"; private static final String UPDATE_NEW_COUNT_OLD = "UPDATE " + TABLE_THREADS + " SET \"new\" = (" + "SELECT COUNT(_id) FROM " + TABLE_MESSAGES + " WHERE thread_id = old.thread_id " + "AND \"new\" <> 0) WHERE _id = old.thread_id"; /** Updates the thread status reflected by the latest message. */ /* private static final String UPDATE_STATUS_OLD = "UPDATE " + TABLE_THREADS + " SET status = (" + "SELECT status FROM " + TABLE_MESSAGES + " WHERE thread_id = old.thread_id ORDER BY timestamp DESC LIMIT 1)" + " WHERE _id = old.thread_id"; */ private static final String UPDATE_STATUS_NEW = "UPDATE " + TABLE_THREADS + " SET status = (" + "SELECT status FROM " + TABLE_MESSAGES + " WHERE thread_id = new.thread_id ORDER BY timestamp DESC LIMIT 1)" + " WHERE _id = new.thread_id"; /** This trigger will update the threads table counters on INSERT. */ private static final String TRIGGER_THREADS_INSERT_COUNT = "CREATE TRIGGER update_thread_on_insert AFTER INSERT ON " + TABLE_MESSAGES + " BEGIN " + UPDATE_MESSAGES_COUNT_NEW + ";" + UPDATE_UNREAD_COUNT_NEW + ";" + UPDATE_NEW_COUNT_NEW + ";" + UPDATE_STATUS_NEW + ";" + "END"; /** This trigger will update the threads table counters on UPDATE. */ private static final String TRIGGER_THREADS_UPDATE_COUNT = "CREATE TRIGGER update_thread_on_update AFTER UPDATE ON " + TABLE_MESSAGES + " BEGIN " + UPDATE_MESSAGES_COUNT_NEW + ";" + UPDATE_UNREAD_COUNT_NEW + ";" + UPDATE_NEW_COUNT_NEW + ";" + UPDATE_STATUS_NEW + ";" + "END"; /** Delete group members linked to thread. */ private static final String DELETE_GROUP_MEMBERS = "DELETE FROM " + TABLE_GROUP_MEMBERS + " WHERE " + Groups.GROUP_JID + "=old." + Groups.GROUP_JID; /** This trigger will update the threads table counters on DELETE. */ private static final String TRIGGER_THREADS_DELETE_COUNT = "CREATE TRIGGER update_thread_on_delete AFTER DELETE ON " + TABLE_MESSAGES + " BEGIN " + UPDATE_MESSAGES_COUNT_OLD + ";" + UPDATE_UNREAD_COUNT_OLD + ";" + UPDATE_NEW_COUNT_OLD + ";" + // do not call this here -- UPDATE_STATUS_OLD + ";" + "END"; /** This trigger will delete group members when a group is deleted. */ private static final String TRIGGER_GROUPS_DELETE_MEMBERS = "CREATE TRIGGER delete_groups_on_delete AFTER DELETE ON " + TABLE_GROUPS + " BEGIN " + DELETE_GROUP_MEMBERS + ";" + "END"; // -- schema upgrades -- // the number in the constant name is the version we are upgrading *from* private static final String[] SCHEMA_UPGRADE_V8 = { "CREATE TABLE groups (" + "group_jid TEXT NOT NULL PRIMARY KEY, " + "thread_id INTEGER NOT NULL," + "group_type TEXT NOT NULL," + "subject TEXT" + ")", "CREATE TABLE group_members (" + "group_jid TEXT NOT NULL, " + "group_peer TEXT NOT NULL, " + "pending INTEGER NOT NULL DEFAULT 0," + "PRIMARY KEY (group_jid, group_peer)" + ")", "CREATE VIEW messages_groups AS " + "SELECT messages.*," + "groups.group_jid," + "groups.subject," + "groups.group_type" + " FROM messages LEFT JOIN threads" + " ON messages.thread_id=threads._id" + " LEFT OUTER JOIN groups ON " + "threads._id=groups.thread_id", "CREATE TRIGGER delete_groups_on_delete AFTER DELETE ON groups" + " BEGIN " + "DELETE FROM group_members WHERE group_jid=old.group_jid;" + "END", }; private static final String[] SCHEMA_UPGRADE_V9 = { "ALTER TABLE groups ADD COLUMN membership INTEGER NOT NULL DEFAULT 1", "DROP VIEW messages_groups", "CREATE VIEW messages_groups AS " + "SELECT messages.*," + "groups.group_jid," + "groups.subject," + "groups.group_type," + "groups.membership" + " FROM messages LEFT JOIN threads" + " ON messages.thread_id=threads._id" + " LEFT OUTER JOIN groups ON " + "threads._id=groups.thread_id", }; private static final String[] SCHEMA_UPGRADE_V10 = { "ALTER TABLE threads ADD COLUMN sticky INTEGER NOT NULL DEFAULT 0", }; private static final String SCHEMA_UPGRADE_V11 = "ALTER TABLE threads ADD COLUMN encryption INTEGER NOT NULL DEFAULT 1"; private Context mContext; protected DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SCHEMA_MESSAGES); db.execSQL(SCHEMA_THREADS); db.execSQL(SCHEMA_GROUPS); db.execSQL(SCHEMA_GROUPS_MEMBERS); db.execSQL(SCHEMA_MESSAGES_GROUPS); db.execSQL(SCHEMA_FULLTEXT); db.execSQL(SCHEMA_MESSAGES_INDEX); db.execSQL(SCHEMA_MESSAGES_TIMESTAMP_IDX); db.execSQL(TRIGGER_THREADS_INSERT_COUNT); db.execSQL(TRIGGER_THREADS_UPDATE_COUNT); db.execSQL(TRIGGER_THREADS_DELETE_COUNT); db.execSQL(TRIGGER_GROUPS_DELETE_MEMBERS); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 8) { // unsupported version throw new SQLException("database can only be upgraded from versions greater than 7"); } switch (oldVersion) { case 8: for (String sql : SCHEMA_UPGRADE_V8) { db.execSQL(sql); } // fall through case 9: for (String sql : SCHEMA_UPGRADE_V9) { db.execSQL(sql); } // fall through case 10: for (String sql : SCHEMA_UPGRADE_V10) { db.execSQL(sql); } // fall through case 11: db.execSQL(SCHEMA_UPGRADE_V11); } } } @Override public boolean onCreate() { dbHelper = new DatabaseHelper(getContext()); return true; } @Override public void shutdown() { dbHelper.close(); } @Override public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLitePagedQueryBuilder qb = new SQLitePagedQueryBuilder(); switch (sUriMatcher.match(uri)) { case MESSAGES: qb.setTables(TABLE_MESSAGES_GROUPS); qb.setProjectionMap(messagesProjectionMap); break; case MESSAGES_ID: qb.setTables(TABLE_MESSAGES_GROUPS); qb.setProjectionMap(messagesProjectionMap); qb.appendWhere(Messages._ID + "=" + uri.getPathSegments().get(1)); break; case MESSAGES_SERVERID: qb.setTables(TABLE_MESSAGES_GROUPS); qb.setProjectionMap(messagesProjectionMap); qb.appendWhere(Messages.MESSAGE_ID + "='" + DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)) + "'"); break; case THREADS: qb.setTables(TABLE_THREADS_GROUPS); qb.setProjectionMap(threadsProjectionMap); break; case THREADS_ID: qb.setTables(TABLE_THREADS_GROUPS); qb.setProjectionMap(threadsProjectionMap); qb.appendWhere(Threads._ID + "=" + uri.getPathSegments().get(1)); break; case THREADS_PEER: qb.setTables(TABLE_THREADS_GROUPS); qb.setProjectionMap(threadsProjectionMap); qb.appendWhere(Threads.PEER + "='" + DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)) + "'"); break; case CONVERSATIONS_ID: // page row count int count = 0; // last ID (scrolling cursor) int lastId = 0; try { lastId = Integer.parseInt(uri.getQueryParameter("last")); } catch (Exception ignored) { } try { count = Integer.parseInt(uri.getQueryParameter("count")); } catch (Exception ignored) { } // setup page if requested if (count > 0) { qb.setPage(count, Messages._ID, lastId); } qb.setTables(TABLE_MESSAGES_GROUPS); qb.setProjectionMap(messagesProjectionMap); qb.appendWhere(Messages.THREAD_ID + "=" + uri.getPathSegments().get(1)); break; case FULLTEXT_ID: qb.setTables(TABLE_FULLTEXT); qb.setProjectionMap(fulltextProjectionMap); qb.appendWhere(Fulltext.CONTENT + " MATCH ?"); selectionArgs = new String[] { uri.getQueryParameter("pattern") }; break; case GROUPS_ID: qb.setTables(TABLE_GROUPS); qb.setProjectionMap(groupsProjectionMap ); qb.appendWhere(Groups.GROUP_JID + "=?"); if (selectionArgs != null) { // conditions appended here will get added before the caller-supplied selection selectionArgs = SystemUtils.concatenate(new String[] { uri.getLastPathSegment() }, selectionArgs); } else { selectionArgs = new String[] { uri.getLastPathSegment() }; } break; case GROUPS_MEMBERS: qb.setTables(TABLE_GROUP_MEMBERS); qb.setProjectionMap(groupsMembersProjectionMap); qb.appendWhere(Groups.GROUP_JID + "=?"); if (selectionArgs != null) { // conditions appended here will get added before the caller-supplied selection selectionArgs = SystemUtils.concatenate(new String[] { uri.getPathSegments().get(1) }, selectionArgs); } else { selectionArgs = new String[] { uri.getPathSegments().get(1) }; } break; default: throw new IllegalArgumentException("Unknown URI " + uri); } SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public synchronized Uri insert(@NonNull Uri uri, ContentValues initialValues) { if (initialValues == null) throw new IllegalArgumentException("No data"); // only messages and requests virtual table can be inserted int match = sUriMatcher.match(uri); if (match != MESSAGES && match != REQUESTS && match != GROUPS && match != GROUPS_MEMBERS) throw new IllegalArgumentException("Unknown URI " + uri); // if this column is present, we'll insert the thread only String draft = initialValues.getAsString(Threads.DRAFT); ContentValues values = new ContentValues(initialValues); SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean success = false; List<Uri> notifications = new ArrayList<>(); beginTransaction(db); try { switch (match) { case GROUPS: // configure thread as group insertGroup(db, values, notifications); success = setTransactionSuccessful(db); // no uri needed return null; case GROUPS_MEMBERS: // insert members into group String groupJid = uri.getPathSegments().get(1); insertGroupMembers(db, groupJid, values); success = setTransactionSuccessful(db); // no uri needed return null; } // we need to know if there previously was a pending request // so we can decide if we have to fire a notification or not boolean requestExists = false; if (match == REQUESTS) { requestExists = isRequestPending(db, initialValues.getAsString(CommonColumns.PEER)); } // create the thread first long threadId = updateThreads(db, values, notifications, match == REQUESTS); values.put(Messages.THREAD_ID, threadId); if (threadId != Messages.NO_THREAD && (draft != null || match == REQUESTS)) { // notify thread change notifications.add(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId)); // notify conversation change notifications.add(ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId)); success = setTransactionSuccessful(db); // draft or request - return conversation return (draft != null || !requestExists) ? ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId) : null; } // remove reserved columns values.remove(Groups.GROUP_JID); values.remove(Groups.SUBJECT); values.remove(Groups.GROUP_TYPE); values.remove(Threads.ENCRYPTION); // insert the new message now! long rowId = db.insertOrThrow(TABLE_MESSAGES, null, values); /* * this will be useful one day perhaps :) long rowId = 0; try { rowId = db.insert(TABLE_MESSAGES, null, values); } catch (SQLiteConstraintException e) { // unique constraint is on msg_id, direction // already stored message, skip it and return uri Cursor c = null; try { c = db.query(TABLE_MESSAGES, new String[] { Messages._ID }, Messages.MESSAGE_ID + " = ? AND " + Messages.DIRECTION + " = ?", new String[] { values.getAsString(Messages.MESSAGE_ID), values.getAsString(Messages.DIRECTION) }, null, null, null, "1"); if (c.moveToFirst()) { rowId = c.getLong(0); return ContentUris.withAppendedId(uri, rowId); } } finally { try { c.close(); } catch (Exception eClose) { // ignore exception } } // message not found (WHAT???) throw e; } */ if (rowId > 0) { if (threadId != Messages.NO_THREAD) { // update fulltext table byte[] content = values.getAsByteArray(Messages.BODY_CONTENT); String mime = values.getAsString(Messages.BODY_MIME); Boolean encrypted = values.getAsBoolean(Messages.ENCRYPTED); if (content != null && content.length > 0 && TextComponent.MIME_TYPE.equals(mime) && (encrypted == null || !encrypted)) { updateFulltext(db, rowId, threadId, content); } } Uri msgUri = ContentUris.withAppendedId(uri, rowId); notifications.add(msgUri); if (threadId != Messages.NO_THREAD) { // notify thread change notifications.add(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId)); // notify conversation change notifications.add(ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId)); } success = setTransactionSuccessful(db); return msgUri; } throw new SQLException("Failed to insert row into " + uri); } finally { endTransaction(db, success); ContentResolver cr = getContext().getContentResolver(); for (Uri nuri : notifications) cr.notifyChange(nuri, null); } } private void insertGroup(SQLiteDatabase db, ContentValues values, List<Uri> notifications) { if (notifications != null) { long threadId = values.getAsLong(Groups.THREAD_ID); // notify thread change notifications.add(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId)); // notify conversation change notifications.add(ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId)); } db.insertOrThrow(TABLE_GROUPS, null, values); } private void insertGroupMembers(SQLiteDatabase db, String groupJid, ContentValues values) { // TODO shouldn't we notify someone? try { values.put(Groups.GROUP_JID, groupJid); db.insertOrThrow(TABLE_GROUP_MEMBERS, null, values); } catch (SQLiteConstraintException e) { // just ignore dups - it doesn't really matter } } private boolean isRequestPending(SQLiteDatabase db, String peer) { Cursor c = null; try { c = db.query(TABLE_THREADS, new String[] { Threads.REQUEST_STATUS }, Threads.PEER + "=?", new String[] { peer }, null, null, null); return c.moveToFirst() && c.getInt(0) == Threads.REQUEST_WAITING; } catch (Exception e) { // ignored } finally { if (c != null) c.close(); } return false; } /** Used to determine content and mime type for a thread. */ private void setThreadContent(byte[] bodyContent, String bodyMime, String attachmentMime, String peer, ContentValues values) { String mime; String content; // use the binary content converted to string if (bodyContent == null) { // try the attachment mime mime = attachmentMime; // no content content = null; } else { // use body data if there is indeed a mime if (bodyMime != null) { mime = bodyMime; content = new String(bodyContent); } // no mime and no data, nothing to do else { mime = null; content = null; } } if (peer != null) { String newContent = peer + ";"; if (content != null) newContent += content; content = newContent; } values.put(Threads.CONTENT, content); values.put(Threads.MIME, mime); } /** * Updates the threads table, returning the thread id to associate with the new message. * A thread is created for the given message if not found. * @return the thread id */ private long updateThreads(SQLiteDatabase db, ContentValues initialValues, List<Uri> notifications, boolean requestOnly) { long threadId = -1; if (initialValues.containsKey(Messages.THREAD_ID)) { threadId = initialValues.getAsLong(Messages.THREAD_ID); if (threadId == Messages.NO_THREAD) return Messages.NO_THREAD; } ContentValues values = new ContentValues(); // group JID will be the thread peer in this case String peer; String groupJid = initialValues.getAsString(Groups.GROUP_JID); if (groupJid != null) peer = groupJid; else peer = initialValues.getAsString(CommonColumns.PEER); values.put(Threads.PEER, peer); values.put(Threads.TIMESTAMP, initialValues.getAsLong(Messages.TIMESTAMP)); if (initialValues.containsKey(Threads.ENCRYPTION)) values.put(Threads.ENCRYPTION, initialValues.getAsBoolean(Threads.ENCRYPTION)); if (requestOnly) { values.put(Threads.MESSAGE_ID, ""); values.put(Threads.DIRECTION, Messages.DIRECTION_IN); values.put(Threads.ENCRYPTED, false); values.put(Threads.REQUEST_STATUS, Threads.REQUEST_WAITING); } else { values.put(Threads.MESSAGE_ID, initialValues.getAsString(Messages.MESSAGE_ID)); values.put(Threads.DIRECTION, initialValues.getAsInteger(Messages.DIRECTION)); values.put(Threads.ENCRYPTED, initialValues.getAsBoolean(Messages.ENCRYPTED)); if (initialValues.containsKey(Messages.STATUS)) values.put(Threads.STATUS, initialValues.getAsInteger(Messages.STATUS)); if (initialValues.containsKey(Messages.STATUS_CHANGED)) values.put(Threads.STATUS_CHANGED, initialValues.getAsInteger(Messages.STATUS_CHANGED)); // this column is an exception if (initialValues.containsKey(Threads.DRAFT)) { String draft = initialValues.getAsString(Threads.DRAFT); if (draft != null && draft.length() == 0) values.putNull(Threads.DRAFT); else values.put(Threads.DRAFT, draft); } // unread column will be calculated by the trigger // thread content has a special behaviour int direction = initialValues.getAsInteger(Messages.DIRECTION); setThreadContent( initialValues.getAsByteArray(Messages.BODY_CONTENT), initialValues.getAsString(Messages.BODY_MIME), initialValues.getAsString(Messages.ATTACHMENT_MIME), direction == Messages.DIRECTION_IN && groupJid != null ? initialValues.getAsString(CommonColumns.PEER) : null, values); } // insert new thread try { threadId = db.insertOrThrow(TABLE_THREADS, null, values); // insert group info if needed if (groupJid != null) { ContentValues groupValues = new ContentValues(); groupValues.put(Groups.GROUP_JID, groupJid); groupValues.put(Groups.THREAD_ID, threadId); groupValues.put(Groups.SUBJECT, initialValues.getAsString(Groups.SUBJECT)); groupValues.put(Groups.GROUP_TYPE, initialValues.getAsString(Groups.GROUP_TYPE)); insertGroup(db, groupValues, null); } // notify newly created thread by userid // this will be used for fixing ticket #18 notifications.add(Threads.getUri(peer)); } catch (SQLException e) { //Log.w(TAG, "error updating thread: " + e.getClass(), e); // clear draft if outgoing message Integer direction = values.getAsInteger(Threads.DIRECTION); if (direction != null && direction == Messages.DIRECTION_OUT) values.putNull(Threads.DRAFT); // remove other stuff coming from subscription request entry if (requestOnly) { values.remove(Threads.MESSAGE_ID); values.remove(Threads.ENCRYPTED); values.remove(Threads.DIRECTION); } db.update(TABLE_THREADS, values, "peer = ?", new String[] { peer }); // the client did not pass the thread id, query for it manually if (threadId < 0) { Cursor c = db.query(TABLE_THREADS, new String[] { Threads._ID }, "peer = ?", new String[] { peer }, null, null, null); if (c.moveToFirst()) threadId = c.getLong(0); c.close(); } } return threadId; } @Override public synchronized int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { String table; String where; String[] args; boolean requestOnly = false; switch (sUriMatcher.match(uri)) { case MESSAGES: table = TABLE_MESSAGES; where = selection; args = selectionArgs; break; case MESSAGES_ID: { long _id = ContentUris.parseId(uri); table = TABLE_MESSAGES; where = Messages._ID + " = ?"; // WARNING selectionArgs is not supported yet if (selection != null) where += " AND (" + selection + ")"; args = new String[] { String.valueOf(_id) }; break; } case MESSAGES_SERVERID: { String messageId = uri.getPathSegments().get(1); table = TABLE_MESSAGES; where = Messages.MESSAGE_ID + " = ?"; // WARNING selectionArgs is not supported yet if (selection != null) where += " AND (" + selection + ")"; args = new String[] { String.valueOf(messageId) }; break; } case THREADS_ID: { long _id = ContentUris.parseId(uri); table = TABLE_THREADS; where = Threads._ID + " = ?"; /* TODO args copy if (selection != null) where += " AND (" + selection + ")"; */ args = new String[] { String.valueOf(_id) }; break; } case REQUESTS: { table = TABLE_THREADS; where = selection; args = selectionArgs; requestOnly = true; break; } case GROUPS: { table = TABLE_GROUPS; where = selection; args = selectionArgs; break; } case GROUPS_ID: { table = TABLE_GROUPS; String groupId = uri.getLastPathSegment(); where = Groups.GROUP_JID + " = ?"; args = new String[] { groupId }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; } case GROUPS_MEMBERS: { String groupId = uri.getPathSegments().get(1); table = TABLE_GROUP_MEMBERS; where = Groups.GROUP_JID + " = ?"; args = new String[] { groupId }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; } case GROUPS_MEMBERS_ID: { table = TABLE_GROUP_MEMBERS; where = Groups.GROUP_JID + " = ? AND " + Groups.PEER + " = ?"; args = new String[] { uri.getPathSegments().get(1), uri.getLastPathSegment() }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; } default: throw new IllegalArgumentException("Unknown URI " + uri); } List<Uri> notifications = null; boolean success = false; SQLiteDatabase db = dbHelper.getWritableDatabase(); beginTransaction(db); try { // handle clear pending flags String pendingFlags = uri.getQueryParameter(Messages.CLEAR_PENDING); if (pendingFlags != null) { updatePendingFlags(db, Integer.parseInt(pendingFlags), where, args); success = setTransactionSuccessful(db); return 0; } // retrieve old data for notifying. // This was done because of the update call could make the old where // condition not working any more. String[] msgIdList = null; if (table.equals(TABLE_MESSAGES)) { // preserve a list of the matching messages for notification and // fulltext update later Cursor old = db.query(TABLE_MESSAGES, new String[] { Messages._ID }, where, args, null, null, null); msgIdList = new String[old.getCount()]; int i = 0; while (old.moveToNext()) { msgIdList[i] = old.getString(0); i++; } old.close(); } int rows = db.update(table, values, where, args); // notify change only if rows are actually affected if (rows > 0) { if (requestOnly) uri = Threads.CONTENT_URI; notifications = new ArrayList<>(); notifications.add(uri); if (table.equals(TABLE_MESSAGES)) { // update fulltext only if content actually changed boolean doUpdateFulltext; String[] projection; byte[] oldContent = values.getAsByteArray(Messages.BODY_CONTENT); if (oldContent != null) { doUpdateFulltext = true; projection = new String[] { Messages.THREAD_ID, Messages._ID, Messages.DIRECTION, Messages.ENCRYPTED, Messages.BODY_CONTENT }; } else { doUpdateFulltext = false; projection = new String[] { Messages.THREAD_ID }; } // build new IN where condition if (msgIdList.length > 0) { StringBuilder whereBuilder = new StringBuilder(Messages._ID + " IN (?"); for (int i = 1; i < msgIdList.length; i++) whereBuilder.append(",?"); whereBuilder.append(")"); Cursor c = db.query(TABLE_MESSAGES, projection, whereBuilder.toString(), msgIdList, null, null, null); while (c.moveToNext()) { long threadId = c.getLong(0); updateThreadInfo(db, threadId, notifications); // update fulltext if necessary if (doUpdateFulltext) { int direction = c.getInt(2); int encrypted = c.getInt(3); if (direction != Messages.DIRECTION_IN || encrypted == 0) updateFulltext(db, c.getLong(1), threadId, c.getBlob(4)); } } c.close(); } } // delete thread if no messages are found else if (requestOnly) { Cursor th = db.query(TABLE_THREADS, new String[] { Threads.COUNT }, where, args, null, null, null); if (th.moveToFirst() && th.getInt(0) == 0) db.delete(TABLE_THREADS, where, args); th.close(); } } success = setTransactionSuccessful(db); return rows; } finally { endTransaction(db, success); if (notifications != null) { ContentResolver cr = getContext().getContentResolver(); for (Uri nuri : notifications) cr.notifyChange(nuri, null); } } } /** Updates group status pending flags. */ private void updatePendingFlags(SQLiteDatabase db, int flags, String where, String[] args) { db.execSQL("UPDATE " + TABLE_GROUP_MEMBERS + " SET pending = pending & ~("+flags+") WHERE " + where, args); } private void updateFulltext(SQLiteDatabase db, long id, long threadId, byte[] content) { // use the binary content converted to string String text = new String(content); ContentValues fulltext = new ContentValues(); fulltext.put(Fulltext._ID, id); fulltext.put(Fulltext.THREAD_ID, threadId); fulltext.put(Fulltext.CONTENT, text); db.replace(TABLE_FULLTEXT, null, fulltext); } @Override public synchronized int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { String table; String where; String[] args; long _id; switch (sUriMatcher.match(uri)) { case MESSAGES: table = TABLE_MESSAGES; where = selection; args = selectionArgs; break; case MESSAGES_ID: table = TABLE_MESSAGES; _id = ContentUris.parseId(uri); where = "_id = ?"; args = new String[] { String.valueOf(_id) }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; case MESSAGES_SERVERID: table = TABLE_MESSAGES; String sid = uri.getPathSegments().get(1); where = "msg_id = ?"; args = new String[] { String.valueOf(sid) }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; case THREADS: table = TABLE_THREADS; where = selection; args = selectionArgs; break; case THREADS_ID: table = TABLE_THREADS; _id = ContentUris.parseId(uri); where = "_id = ?"; args = new String[] { String.valueOf(_id) }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; case THREADS_PEER: table = TABLE_THREADS; where = "peer = ?"; args = new String[] { uri.getLastPathSegment() }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; case GROUPS_ID: table = TABLE_GROUPS; where = Groups.GROUP_JID + "=?"; args = new String[] { uri.getLastPathSegment() }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; case GROUPS_MEMBERS_ID: table = TABLE_GROUP_MEMBERS; where = Groups.GROUP_JID + " = ? AND " + Groups.PEER + " = ?"; args = new String[] { uri.getPathSegments().get(1), uri.getLastPathSegment() }; if (selection != null) { where += " AND (" + selection + ")"; if (selectionArgs != null) args = SystemUtils.concatenate(args, selectionArgs); } break; // special case: conversations case CONVERSATIONS_ID: { boolean keepGroup = Boolean.parseBoolean(uri.getQueryParameter(Messages.KEEP_GROUP)); int rows = deleteConversation(uri, keepGroup); if (rows > 0) { ContentResolver cr = getContext().getContentResolver(); // first of all, notify conversation cr.notifyChange(uri, null); // then notify thread itself long threadId = ContentUris.parseId(uri); cr.notifyChange(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId), null); } return rows; // END :) } // special case: delete all content case CONVERSATIONS_ALL_ID: { SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean keepGroup = Boolean.parseBoolean(uri.getQueryParameter(Messages.KEEP_GROUP)); boolean success = false; int num = 0; beginTransaction(db); try { // rows count will be conversations num = db.delete(TABLE_THREADS, null, null); db.delete(TABLE_MESSAGES, null, null); // update fulltext db.delete(TABLE_FULLTEXT, null, null); if (!keepGroup) { // delete groups (members will cascade) db.delete(TABLE_GROUPS, null, null); } // set transaction successful success = setTransactionSuccessful(db); } finally { endTransaction(db, success); } if (num > 0) { ContentResolver cr = getContext().getContentResolver(); // notify conversations and threads cr.notifyChange(uri, null); cr.notifyChange(Threads.CONTENT_URI, null); } return num; } default: throw new IllegalArgumentException("Unknown URI " + uri); } int rows = 0; SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean success = false; List<Uri> notifications = new ArrayList<>(); // let's begin this big transaction :S beginTransaction(db); try { long threadId = -1; if (table.equals(TABLE_MESSAGES)) { // retrieve the thread id for later use by updateThreadInfo(), and // also update fulltext table Cursor c = db.query(TABLE_MESSAGES, new String[] { Messages.THREAD_ID, Messages._ID, Messages.DIRECTION, Messages.ENCRYPTED }, where, args, null, null, null); if (c != null) { while (c.moveToNext()) { // FIXME this way we'll only get one threadId... threadId = c.getLong(0); // update fulltext int direction = c.getInt(2); int encrypted = c.getInt(3); if (direction != Messages.DIRECTION_IN || encrypted == 0) db.delete(TABLE_FULLTEXT, Fulltext._ID + " = " + c.getLong(1), null); } c.close(); } } // DELETE! rows = db.delete(table, where, args); // notify change only if rows are actually affected if (rows > 0) notifications.add(uri); if (table.equals(TABLE_MESSAGES)) { updateThreadAfterDelete(db, threadId, notifications); } success = setTransactionSuccessful(db); } finally { endTransaction(db, success); ContentResolver cr = getContext().getContentResolver(); for (Uri nuri : notifications) cr.notifyChange(nuri, null); } return rows; } private void updateThreadAfterDelete(SQLiteDatabase db, long threadId, @Nullable List<Uri> notifications) { // check for empty threads if (deleteEmptyThreads(db) > 0 && notifications != null) notifications.add(Threads.CONTENT_URI); // update thread with latest info and status if (threadId > 0) { updateThreadInfo(db, threadId, notifications); } else Log.e(TAG, "unable to update thread metadata (threadId not found)"); // change notifications get triggered by previous method calls } private int deleteConversation(Uri uri, boolean keepGroup) { long threadId = ContentUris.parseId(uri); if (threadId > 0) { SQLiteDatabase db = dbHelper.getWritableDatabase(); boolean success = false; beginTransaction(db); try { int num = 0; if (!keepGroup) num = db.delete(TABLE_THREADS, Threads._ID + " = " + threadId, null); // exclude group commands from delete if we are keeping the group String where = Messages.THREAD_ID + " = " + threadId; String[] args = null; if (keepGroup) { where += " AND (" + Messages.BODY_MIME + " <> ? OR " + Messages.BODY_MIME + " IS NULL)"; args = new String[] { GroupCommandComponent.MIME_TYPE }; } num += db.delete(TABLE_MESSAGES, where, args); if (!keepGroup) // delete group if requested num += db.delete(TABLE_GROUPS, Groups.THREAD_ID + " = " + threadId, null); // update fulltext db.delete(TABLE_FULLTEXT, Messages.THREAD_ID + " = " + threadId, null); // update thread information updateThreadAfterDelete(db, threadId, null); // set transaction successful success = setTransactionSuccessful(db); return num; } finally { endTransaction(db, success); } } return -1; } /** Updates metadata of a given thread. */ private int updateThreadInfo(SQLiteDatabase db, long threadId, @Nullable List<Uri> notifications) { Cursor c = db.query(TABLE_MESSAGES_GROUPS, new String[] { Messages.MESSAGE_ID, Messages.DIRECTION, Messages.STATUS, Messages.BODY_CONTENT, Messages.BODY_MIME, Messages.ATTACHMENT_MIME, Messages.TIMESTAMP, Messages.SERVER_TIMESTAMP, Messages.PEER, Groups.GROUP_JID, }, Messages.THREAD_ID + " = ?", new String[] { String.valueOf(threadId) }, null, null, Messages.INVERTED_SORT_ORDER, "1"); int rc = -1; if (c != null) { ContentValues v = new ContentValues(); if (c.moveToFirst()) { int direction = c.getInt(1); v.put(Threads.MESSAGE_ID, c.getString(0)); v.put(Threads.DIRECTION, direction); v.put(Threads.STATUS, c.getInt(2)); String groupJid = c.getString(9); String peer = (groupJid != null && direction == Messages.DIRECTION_IN) ? c.getString(8) : null; setThreadContent(c.getBlob(3), c.getString(4), c.getString(5), peer, v); // use server timestamp if present long ts = c.getLong(7); v.put(Threads.TIMESTAMP, ts > 0 ? ts : c.getLong(6)); } else { // empty thread data v.put(Threads.MESSAGE_ID, "draft" + (new Random().nextInt())); v.put(Threads.DIRECTION, Messages.DIRECTION_OUT); v.put(Threads.TIMESTAMP, System.currentTimeMillis()); v.putNull(Threads.STATUS); setThreadContent(new byte[0], TextComponent.MIME_TYPE, null, null, v); } rc = db.update(TABLE_THREADS, v, Threads._ID + "=" + threadId, null); if (rc > 0 && notifications != null) { notifications.add(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId)); notifications.add(ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId)); } c.close(); } return rc; } private int deleteEmptyThreads(SQLiteDatabase db) { return db.delete(TABLE_THREADS, "\"" + Threads.COUNT + "\"" + " = 0 AND " + Threads.DRAFT + " IS NULL AND " + "NOT EXISTS (SELECT 1 FROM " + TABLE_GROUPS + " WHERE "+TABLE_THREADS+"."+Threads._ID+"="+Groups.THREAD_ID+")", null); } @Override public String getType(@NonNull Uri uri) { switch (sUriMatcher.match(uri)) { case MESSAGES: case CONVERSATIONS_ID: return Messages.CONTENT_TYPE; case MESSAGES_ID: return Messages.CONTENT_ITEM_TYPE; case THREADS: return Threads.CONTENT_TYPE; case THREADS_PEER: return Threads.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } /* Transactions compatibility layer */ @TargetApi(android.os.Build.VERSION_CODES.HONEYCOMB) private void beginTransaction(SQLiteDatabase db) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.beginTransactionNonExclusive(); else // this is because API < 11 doesn't have beginTransactionNonExclusive() db.execSQL("BEGIN IMMEDIATE"); } private boolean setTransactionSuccessful(SQLiteDatabase db) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.setTransactionSuccessful(); return true; } private void endTransaction(SQLiteDatabase db, boolean success) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.endTransaction(); else db.execSQL(success ? "COMMIT" : "ROLLBACK"); } public static boolean deleteDatabase(Context ctx) { try { ContentResolver c = ctx.getContentResolver(); c.delete(Conversations.CONTENT_URI, null, null); return true; } catch (Exception e) { Log.e(TAG, "error during database delete!", e); return false; } } /** * Marks all messages of the given thread as read. * @param context used to request a {@link ContentResolver} * @param id the thread id * @return the number of rows affected in the messages table */ public static int markThreadAsRead(Context context, long id) { ContentResolver c = context.getContentResolver(); ContentValues values = new ContentValues(2); values.put(Messages.UNREAD, Boolean.FALSE); values.put(Messages.NEW, Boolean.FALSE); return c.update(Messages.CONTENT_URI, values, Messages.THREAD_ID + " = ? AND " + Messages.UNREAD + " <> 0 AND " + Messages.DIRECTION + " = " + Messages.DIRECTION_IN, new String[] { String.valueOf(id) }); } /** * Marks all messages as read. * @param context used to request a {@link ContentResolver} * @return the number of rows affected in the messages table */ public static int markAllThreadsAsRead(Context context) { ContentResolver c = context.getContentResolver(); ContentValues values = new ContentValues(1); values.put(Messages.NEW, Boolean.FALSE); values.put(Messages.UNREAD, Boolean.FALSE); return c.update(Messages.CONTENT_URI, values, Messages.UNREAD + " <> 0 AND " + Messages.DIRECTION + " = " + Messages.DIRECTION_IN, null); } public static int markThreadAsOld(Context context, long id) { ContentResolver c = context.getContentResolver(); ContentValues values = new ContentValues(1); values.put(Messages.NEW, Boolean.FALSE); return c.update(Messages.CONTENT_URI, values, Messages.THREAD_ID + " = ? AND " + Messages.NEW + " <> 0 AND " + Messages.DIRECTION + " = " + Messages.DIRECTION_IN, new String[] { String.valueOf(id) }); } /** * Marks all messages as old. * @param context used to request a {@link ContentResolver} * @return the number of rows affected in the messages table */ public static int markAllThreadsAsOld(Context context) { ContentResolver c = context.getContentResolver(); ContentValues values = new ContentValues(1); values.put(Messages.NEW, Boolean.FALSE); return c.update(Messages.CONTENT_URI, values, Messages.NEW + " <> 0 AND " + Messages.DIRECTION + " = " + Messages.DIRECTION_IN, null); } private static ContentValues prepareChangeMessageStatus( int status, long timestamp, long statusChanged) { ContentValues values = new ContentValues(); values.put(Messages.STATUS, status); if (timestamp >= 0) values.put(Messages.TIMESTAMP, timestamp); if (statusChanged >= 0) values.put(Messages.STATUS_CHANGED, statusChanged); return values; } public static int changeMessageStatus(Context context, Uri uri, int direction, int status) { return changeMessageStatus(context, uri, direction, status, -1, -1); } public static int changeMessageStatus(Context context, Uri uri, int direction, int status, long timestamp, long statusChanged) { ContentValues values = prepareChangeMessageStatus(status, timestamp, statusChanged); return context.getContentResolver().update(uri, values, Messages.DIRECTION + "=" + direction, null); } public static int changeMessageStatus(Context context, long id, int status) { return changeMessageStatus(context, id, status, -1, -1); } public static int changeMessageStatus(Context context, long id, int status, long timestamp, long statusChanged) { ContentValues values = prepareChangeMessageStatus(status, timestamp, statusChanged); Uri uri = ContentUris.withAppendedId(Messages.CONTENT_URI, id); return context.getContentResolver().update(uri, values, null, null); } /* public static int changeMessageStatus(Context context, String id, int direction, boolean realId, int status) { return changeMessageStatus(context, id, direction, realId, status, -1, -1); } public static int changeMessageStatus(Context context, String id, int direction, boolean realId, int status, long timestamp, long statusChanged) { ContentValues values = prepareChangeMessageStatus(status, timestamp, statusChanged); String field = (realId) ? Messages.REAL_ID : Messages.MESSAGE_ID; return context.getContentResolver().update(Messages.CONTENT_URI, values, field + " = ? AND " + Messages.DIRECTION + "=" + direction, new String[] { id }); } */ /** Update a message status if old status == whereStatus. */ /* public static int changeMessageStatusWhere(Context context, boolean notEquals, int whereStatus, String id, boolean realId, int status, long timestamp, long statusChanged) { ContentValues values = prepareChangeMessageStatus(status, timestamp, statusChanged); String field = (realId) ? Messages.REAL_ID : Messages.MESSAGE_ID; String op = (notEquals) ? "<>" : "="; return context.getContentResolver().update(Messages.CONTENT_URI, values, field + " = ? AND " + Messages.STATUS + op + whereStatus, new String[] { id }); } */ /** Set the fetch URL of a media message, marking it as uploaded. */ public static void uploaded(Context context, long msgId, String fetchUrl) { ContentValues values = new ContentValues(1); values.put(Messages.ATTACHMENT_FETCH_URL, fetchUrl); context.getContentResolver().update(Messages.CONTENT_URI, values, Messages._ID + " = " + msgId, null); } static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(AUTHORITY, TABLE_THREADS, THREADS); sUriMatcher.addURI(AUTHORITY, TABLE_THREADS + "/#", THREADS_ID); sUriMatcher.addURI(AUTHORITY, TABLE_THREADS + "/*", THREADS_PEER); sUriMatcher.addURI(AUTHORITY, TABLE_MESSAGES, MESSAGES); sUriMatcher.addURI(AUTHORITY, TABLE_MESSAGES + "/#", MESSAGES_ID); sUriMatcher.addURI(AUTHORITY, TABLE_MESSAGES + "/*", MESSAGES_SERVERID); sUriMatcher.addURI(AUTHORITY, "conversations", CONVERSATIONS_ALL_ID); sUriMatcher.addURI(AUTHORITY, "conversations/#", CONVERSATIONS_ID); sUriMatcher.addURI(AUTHORITY, TABLE_GROUPS, GROUPS); sUriMatcher.addURI(AUTHORITY, TABLE_GROUPS + "/*", GROUPS_ID); sUriMatcher.addURI(AUTHORITY, TABLE_GROUPS + "/*/members", GROUPS_MEMBERS); sUriMatcher.addURI(AUTHORITY, TABLE_GROUPS + "/*/members/*", GROUPS_MEMBERS_ID); sUriMatcher.addURI(AUTHORITY, TABLE_FULLTEXT, FULLTEXT_ID); sUriMatcher.addURI(AUTHORITY, "requests", REQUESTS); messagesProjectionMap = new HashMap<>(); messagesProjectionMap.put(Messages._ID, Messages._ID); messagesProjectionMap.put(Messages.THREAD_ID, Messages.THREAD_ID); messagesProjectionMap.put(Messages.MESSAGE_ID, Messages.MESSAGE_ID); messagesProjectionMap.put(Messages.PEER, Messages.PEER); messagesProjectionMap.put(Messages.BODY_MIME, Messages.BODY_MIME); messagesProjectionMap.put(Messages.BODY_CONTENT, Messages.BODY_CONTENT); messagesProjectionMap.put(Messages.BODY_LENGTH, Messages.BODY_LENGTH); messagesProjectionMap.put(Messages.ATTACHMENT_MIME, Messages.ATTACHMENT_MIME); messagesProjectionMap.put(Messages.ATTACHMENT_PREVIEW_PATH, Messages.ATTACHMENT_PREVIEW_PATH); messagesProjectionMap.put(Messages.ATTACHMENT_FETCH_URL, Messages.ATTACHMENT_FETCH_URL); messagesProjectionMap.put(Messages.ATTACHMENT_LOCAL_URI, Messages.ATTACHMENT_LOCAL_URI); messagesProjectionMap.put(Messages.ATTACHMENT_LENGTH, Messages.ATTACHMENT_LENGTH); messagesProjectionMap.put(Messages.ATTACHMENT_COMPRESS, Messages.ATTACHMENT_COMPRESS); messagesProjectionMap.put(Messages.ATTACHMENT_ENCRYPTED, Messages.ATTACHMENT_ENCRYPTED); messagesProjectionMap.put(Messages.ATTACHMENT_SECURITY_FLAGS, Messages.ATTACHMENT_SECURITY_FLAGS); messagesProjectionMap.put(Messages.UNREAD, Messages.UNREAD); messagesProjectionMap.put(Messages.NEW, Messages.NEW); messagesProjectionMap.put(Messages.DIRECTION, Messages.DIRECTION); messagesProjectionMap.put(Messages.TIMESTAMP, Messages.TIMESTAMP); messagesProjectionMap.put(Messages.STATUS_CHANGED, Messages.STATUS_CHANGED); messagesProjectionMap.put(Messages.STATUS, Messages.STATUS); messagesProjectionMap.put(Messages.ENCRYPTED, Messages.ENCRYPTED); messagesProjectionMap.put(Messages.SECURITY_FLAGS, Messages.SECURITY_FLAGS); messagesProjectionMap.put(Messages.SERVER_TIMESTAMP, Messages.SERVER_TIMESTAMP); messagesProjectionMap.put(Groups.GROUP_JID, Groups.GROUP_JID); messagesProjectionMap.put(Groups.SUBJECT, Groups.SUBJECT); messagesProjectionMap.put(Groups.GROUP_TYPE, Groups.GROUP_TYPE); messagesProjectionMap.put(Groups.MEMBERSHIP, Groups.MEMBERSHIP); threadsProjectionMap = new HashMap<>(); threadsProjectionMap.put(Threads._ID, Threads._ID); threadsProjectionMap.put(Threads.MESSAGE_ID, Threads.MESSAGE_ID); threadsProjectionMap.put(Threads.PEER, Threads.PEER); threadsProjectionMap.put(Threads.DIRECTION, Threads.DIRECTION); threadsProjectionMap.put(Threads.COUNT, Threads.COUNT); threadsProjectionMap.put(Threads.UNREAD, Threads.UNREAD); threadsProjectionMap.put(Threads.NEW, Threads.NEW); threadsProjectionMap.put(Threads.MIME, Threads.MIME); threadsProjectionMap.put(Threads.CONTENT, Threads.CONTENT); threadsProjectionMap.put(Threads.TIMESTAMP, Threads.TIMESTAMP); threadsProjectionMap.put(Threads.STATUS_CHANGED, Threads.STATUS_CHANGED); threadsProjectionMap.put(Threads.STATUS, Threads.STATUS); threadsProjectionMap.put(Threads.ENCRYPTED, Threads.ENCRYPTED); threadsProjectionMap.put(Threads.DRAFT, Threads.DRAFT); threadsProjectionMap.put(Threads.REQUEST_STATUS, Threads.REQUEST_STATUS); threadsProjectionMap.put(Threads.STICKY, Threads.STICKY); threadsProjectionMap.put(Threads.ENCRYPTION, Threads.ENCRYPTION); threadsProjectionMap.put(Groups.GROUP_JID, Groups.GROUP_JID); threadsProjectionMap.put(Groups.SUBJECT, Groups.SUBJECT); threadsProjectionMap.put(Groups.GROUP_TYPE, Groups.GROUP_TYPE); threadsProjectionMap.put(Groups.MEMBERSHIP, Groups.MEMBERSHIP); fulltextProjectionMap = new HashMap<>(); fulltextProjectionMap.put(Fulltext.THREAD_ID, Fulltext.THREAD_ID); fulltextProjectionMap.put(Fulltext.CONTENT, Fulltext.CONTENT); groupsProjectionMap = new HashMap<>(); groupsProjectionMap.put(Groups.GROUP_JID, Groups.GROUP_JID); groupsProjectionMap.put(Groups.THREAD_ID, Groups.THREAD_ID); groupsProjectionMap.put(Groups.GROUP_TYPE, Groups.GROUP_TYPE); groupsProjectionMap.put(Groups.SUBJECT, Groups.SUBJECT); groupsProjectionMap.put(Groups.MEMBERSHIP, Groups.MEMBERSHIP); groupsMembersProjectionMap = new HashMap<>(); groupsMembersProjectionMap.put(Groups.GROUP_JID, Groups.GROUP_JID); groupsMembersProjectionMap.put(Groups.PEER, Groups.PEER); groupsMembersProjectionMap.put(Groups.PENDING, Groups.PENDING); } }