gt:                 where = Messages.MESSAGE_ID + " = ?";
context: /*
   * Kontalk Android client
   * Copyright (C) 2015 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 org.kontalk.BuildConfig;
  import org.kontalk.client.EndpointServer;
  import org.kontalk.client.ServerList;
  import org.kontalk.crypto.Coder;
  import org.kontalk.message.CompositeMessage;
  import org.kontalk.provider.MyMessages.CommonColumns;
  import org.kontalk.provider.MyMessages.Messages;
  import org.kontalk.provider.MyMessages.Threads;
  import org.kontalk.provider.MyMessages.Messages.Fulltext;
  import org.kontalk.provider.MyMessages.Threads.Conversations;
  import org.kontalk.service.ServerListUpdater;
  
  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.SQLiteDatabase;
  import android.database.sqlite.SQLiteOpenHelper;
  import android.database.sqlite.SQLiteQueryBuilder;
  import android.net.Uri;
  import android.util.Log;
  
  
  /**
   * 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_FULLTEXT = "fulltext";
      private static final String TABLE_THREADS = "threads";
  
      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 FULLTEXT_ID = 9;
      private static final int REQUESTS = 10;
  
      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 class DatabaseHelper extends SQLiteOpenHelper {
          private static final int DATABASE_VERSION = 8;
          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" +
              ")";
  
          /** This table will contain the latest message from each conversation. */
          private static final String SCHEMA_THREADS =
              "CREATE TABLE " + TABLE_THREADS + " " + _SCHEMA_THREADS;
  
          /** 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";
  
  
          /** 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";
  
          private static final String[] SCHEMA_UPGRADE_V4 = {
              // create temporary messages tables without msg_id UNIQUE constraint
              "CREATE TABLE " + TABLE_MESSAGES + "_new " + _SCHEMA_MESSAGES,
              // create temporary threads tables without msg_id UNIQUE constraint
              "CREATE TABLE " + TABLE_THREADS + "_new " + _SCHEMA_THREADS,
              // copy contents of messages table
              "INSERT INTO " + TABLE_MESSAGES + "_new SELECT " +
              "_id, thread_id, msg_id, SUBSTR(peer, 1, ?) || '@' || ?, direction, unread, 0, timestamp, status_changed, status, 'text/plain', " +
              "CASE WHEN mime <> 'text/plain' THEN NULL ELSE content END, "+
              "CASE WHEN mime <> 'text/plain' THEN 0 ELSE length(content) END, " +
              "CASE WHEN mime <> 'text/plain' THEN mime ELSE NULL END, preview_path, fetch_url, local_uri, length, 0, 0, 0, encrypted, " +
              "CASE WHEN encrypt_key IS NOT NULL THEN " + Coder.SECURITY_LEGACY_ENCRYPTED + " ELSE " + Coder.SECURITY_CLEARTEXT + " END, "+
              "strftime('%s', server_timestamp)*1000" +
                  " FROM " + TABLE_MESSAGES + " WHERE encrypted = 0",
              // copy contents of threads table
              "INSERT INTO " + TABLE_THREADS + "_new SELECT " +
              "_id, msg_id, SUBSTR(peer, 1, ?) || '@' || ?, direction, count, unread, 0, 'text/plain', content, timestamp, status_changed, status, 0, draft, 0" +
                  " FROM " + TABLE_THREADS,
              // drop table messages
              "DROP TABLE " + TABLE_MESSAGES,
              // drop table threads
              "DROP TABLE " + TABLE_THREADS,
              // rename messages_new to messages
              "ALTER TABLE " + TABLE_MESSAGES + "_new RENAME TO " + TABLE_MESSAGES,
              // rename threads_new to threads
              "ALTER TABLE " + TABLE_THREADS + "_new RENAME TO " + TABLE_THREADS,
              // unique message index
              SCHEMA_MESSAGES_INDEX,
              // timestamp message index (for sorting)
              SCHEMA_MESSAGES_TIMESTAMP_IDX,
              // triggers
              TRIGGER_THREADS_INSERT_COUNT,
              TRIGGER_THREADS_UPDATE_COUNT,
              TRIGGER_THREADS_DELETE_COUNT
          };
  
          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_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);
          }
  
          @Override
          public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
              if (oldVersion != 4 && oldVersion < 8) {
                  // unsupported version
                  throw new SQLException("database can only be upgraded from version 4 or versions greather than 7");
              }
  
              if (oldVersion == 4) {
                  // take the first server from the builtin list
                  ServerList list = ServerListUpdater.getCurrentList(mContext);
                  EndpointServer server = list.get(0);
                  String host = server.getNetwork();
  
                  for (String sql : SCHEMA_UPGRADE_V4) {
                      if (sql.startsWith("INSERT ")) {
                          db.execSQL(sql, new Object[]{
                              CompositeMessage.USERID_LENGTH,
                              host
                          });
                      }
                      else {
                          db.execSQL(sql);
                      }
                  }
              }
          }
      }
  
      @Override
      public boolean onCreate() {
          dbHelper = new DatabaseHelper(getContext());
          return true;
      }
  
      @Override
      public Cursor query(Uri uri, String[] projection, String selection,
              String[] selectionArgs, String sortOrder) {
          SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
  
          switch (sUriMatcher.match(uri)) {
              case MESSAGES:
                  qb.setTables(TABLE_MESSAGES);
                  qb.setProjectionMap(messagesProjectionMap);
                  break;
  
              case MESSAGES_ID:
                  qb.setTables(TABLE_MESSAGES);
                  qb.setProjectionMap(messagesProjectionMap);
                  qb.appendWhere(Messages._ID + "=" + uri.getPathSegments().get(1));
                  break;
  
              case MESSAGES_SERVERID:
                  qb.setTables(TABLE_MESSAGES);
                  qb.setProjectionMap(messagesProjectionMap);
                  qb.appendWhere(Messages.MESSAGE_ID + "='" + DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)) + "'");
                  break;
  
              case THREADS:
                  qb.setTables(TABLE_THREADS);
                  qb.setProjectionMap(threadsProjectionMap);
                  break;
  
              case THREADS_ID:
                  qb.setTables(TABLE_THREADS);
                  qb.setProjectionMap(threadsProjectionMap);
                  qb.appendWhere(Threads._ID + "=" + uri.getPathSegments().get(1));
                  break;
  
              case THREADS_PEER:
                  qb.setTables(TABLE_THREADS);
                  qb.setProjectionMap(threadsProjectionMap);
                  qb.appendWhere(Threads.PEER + "='" + DatabaseUtils.sqlEscapeString(uri.getPathSegments().get(1)) + "'");
                  break;
  
              case CONVERSATIONS_ID:
                  qb.setTables(TABLE_MESSAGES);
                  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;
  
              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(Uri uri, ContentValues initialValues) {
          // only messages and requests virtual table can be inserted
          int match = sUriMatcher.match(uri);
          if (match != MESSAGES && match != REQUESTS) { throw new IllegalArgumentException("Unknown URI " + uri); }
          if (initialValues == null) { throw new IllegalArgumentException("No data"); }
  
          // 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<Uri>();
  
          try {
              beginTransaction(db);
  
              // 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);
  
              if (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);
  
                  // request only - return conversation
                  if (match == REQUESTS && !requestExists)
                      return ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId);
  
                  // draft only - no uri
                  else
                      return null;
  
              }
  
              values.put(Messages.THREAD_ID, threadId);
  
              // 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) {
                  // update fulltext table
                  byte[] content = values.getAsByteArray(Messages.BODY_CONTENT);
                  Boolean encrypted = values.getAsBoolean(Messages.ENCRYPTED);
                  if (content != null && content.length > 0 && (encrypted == null || !encrypted.booleanValue())) {
                      updateFulltext(db, rowId, threadId, content);
                  }
  
                  Uri msgUri = ContentUris.withAppendedId(uri, rowId);
                  notifications.add(msgUri);
  
                  // 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 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, 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;
              }
          }
  
          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) {
          ContentValues values = new ContentValues();
          String peer = initialValues.getAsString(CommonColumns.PEER);
  
          long threadId = -1;
          if (initialValues.containsKey(Messages.THREAD_ID))
              threadId = initialValues.getAsLong(Messages.THREAD_ID);
  
          values.put(Threads.PEER, peer);
          values.put(Threads.TIMESTAMP, initialValues.getAsLong(Messages.TIMESTAMP));
  
          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))
                  values.put(Threads.DRAFT, initialValues.getAsString(Threads.DRAFT));
  
              // unread column will be calculated by the trigger
  
              // thread content has a special behaviour
              setThreadContent(
                  initialValues.getAsByteArray(Messages.BODY_CONTENT),
                  initialValues.getAsString(Messages.BODY_MIME),
                  initialValues.getAsString(Messages.ATTACHMENT_MIME),
                  values);
          }
  
          // insert new thread
          try {
              threadId = db.insertOrThrow(TABLE_THREADS, null, values);
  
              // notify newly created thread by userid
              // this will be used for fixing ticket #18
              notifications.add(Threads.getUri(peer));
          }
          catch (SQLException 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(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
          if (values == null) { throw new IllegalArgumentException("No data"); }
  
          String table;
          String where;
          String[] args;
          String messageId = null;
          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:
                  messageId = uri.getPathSegments().get(1);
                  table = TABLE_MESSAGES;
  