/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package info.guardianproject.otr.app.im.provider;
import info.guardianproject.otr.OtrAndroidKeyManagerImpl;
import info.guardianproject.otr.app.im.app.ContactView;
import info.guardianproject.otr.app.im.app.ImApp;
import info.guardianproject.otr.app.im.provider.Imps.Contacts;
import info.guardianproject.otr.app.im.provider.Imps.Provider;
import info.guardianproject.util.Debug;
import info.guardianproject.util.LogCleaner;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.StreamCorruptedException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteQueryBuilder;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.res.Configuration;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
/** A content provider for IM */
public class ImpsProvider extends ContentProvider {
private static final String PREV_DATABASE_OPEN_TRAIL_TAG = "prev_database_open";
private static final String EMPTY_KEY_TRAIL_TAG = "empty_key";
private static final String DATABASE_OPEN_TRAIL_TAG = "database_open";
private static final String LOG_TAG = "imProvider";
private static final String AUTHORITY = "info.guardianproject.otr.app.im.provider.Imps";
private static final String TABLE_ACCOUNTS = "accounts";
private static final String TABLE_PROVIDERS = "providers";
private static final String TABLE_PROVIDER_SETTINGS = "providerSettings";
private static final String TABLE_CONTACTS = "contacts";
private static final String TABLE_CONTACTS_ETAG = "contactsEtag";
private static final String TABLE_BLOCKED_LIST = "blockedList";
private static final String TABLE_CONTACT_LIST = "contactList";
private static final String TABLE_INVITATIONS = "invitations";
private static final String TABLE_GROUP_MEMBERS = "groupMembers";
private static final String TABLE_PRESENCE = "presence";
private static final String USERNAME = "username";
private static final String TABLE_CHATS = "chats";
private static final String TABLE_AVATARS = "avatars";
private static final String TABLE_SESSION_COOKIES = "sessionCookies";
private static final String TABLE_MESSAGES = "messages";
private static final String TABLE_IN_MEMORY_MESSAGES = "inMemoryMessages";
private static final String TABLE_ACCOUNT_STATUS = "accountStatus";
private static final String TABLE_BRANDING_RESOURCE_MAP_CACHE = "brandingResMapCache";
// tables for mcs and rmq
private static final String TABLE_OUTGOING_RMQ_MESSAGES = "outgoingRmqMessages";
private static final String TABLE_LAST_RMQ_ID = "lastrmqid";
private static final String TABLE_S2D_RMQ_IDS = "s2dRmqIds";
private static final String ENCRYPTED_DATABASE_NAME = "impsenc.db";
private static final String UNENCRYPTED_DATABASE_NAME = "imps.db";
private static final int DATABASE_VERSION = 105;
protected static final int MATCH_PROVIDERS = 1;
protected static final int MATCH_PROVIDERS_BY_ID = 2;
protected static final int MATCH_PROVIDERS_WITH_ACCOUNT = 3;
protected static final int MATCH_ACCOUNTS = 10;
protected static final int MATCH_ACCOUNTS_BY_ID = 11;
protected static final int MATCH_ACCOUNTS_WITH_DOMAIN = 12;
protected static final int MATCH_CONTACTS = 18;
protected static final int MATCH_CONTACTS_JOIN_PRESENCE = 19;
protected static final int MATCH_CONTACTS_BAREBONE = 20;
protected static final int MATCH_CHATTING_CONTACTS = 21;
protected static final int MATCH_CONTACTS_BY_PROVIDER = 22;
protected static final int MATCH_CHATTING_CONTACTS_BY_PROVIDER = 23;
protected static final int MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER = 24;
protected static final int MATCH_ONLINE_CONTACTS_BY_PROVIDER = 25;
protected static final int MATCH_OFFLINE_CONTACTS_BY_PROVIDER = 26;
protected static final int MATCH_CONTACT = 27;
protected static final int MATCH_CONTACTS_BULK = 28;
protected static final int MATCH_ONLINE_CONTACT_COUNT = 30;
protected static final int MATCH_BLOCKED_CONTACTS = 31;
protected static final int MATCH_CONTACTLISTS = 32;
protected static final int MATCH_CONTACTLISTS_BY_PROVIDER = 33;
protected static final int MATCH_CONTACTLIST = 34;
protected static final int MATCH_BLOCKEDLIST = 35;
protected static final int MATCH_BLOCKEDLIST_BY_PROVIDER = 36;
protected static final int MATCH_CONTACTS_ETAGS = 37;
protected static final int MATCH_CONTACTS_ETAG = 38;
protected static final int MATCH_PRESENCE = 40;
protected static final int MATCH_PRESENCE_ID = 41;
protected static final int MATCH_PRESENCE_BY_ACCOUNT = 42;
protected static final int MATCH_PRESENCE_SEED_BY_ACCOUNT = 43;
protected static final int MATCH_PRESENCE_BULK = 44;
protected static final int MATCH_MESSAGES = 50;
protected static final int MATCH_MESSAGES_BY_CONTACT = 51;
protected static final int MATCH_MESSAGES_BY_THREAD_ID = 52;
protected static final int MATCH_MESSAGES_BY_PROVIDER = 53;
protected static final int MATCH_MESSAGES_BY_ACCOUNT = 54;
protected static final int MATCH_MESSAGE = 55;
protected static final int MATCH_OTR_MESSAGES = 56;
protected static final int MATCH_OTR_MESSAGES_BY_CONTACT = 57;
protected static final int MATCH_OTR_MESSAGES_BY_THREAD_ID = 58;
protected static final int MATCH_OTR_MESSAGES_BY_PROVIDER = 59;
protected static final int MATCH_OTR_MESSAGES_BY_ACCOUNT = 60;
protected static final int MATCH_OTR_MESSAGE = 61;
protected static final int MATCH_OTR_MESSAGES_BY_PACKET_ID = 62;
protected static final int MATCH_MESSAGES_BY_PACKET_ID = 63;
protected static final int MATCH_GROUP_MEMBERS = 65;
protected static final int MATCH_GROUP_MEMBERS_BY_GROUP = 66;
protected static final int MATCH_AVATARS = 70;
protected static final int MATCH_AVATAR = 71;
protected static final int MATCH_AVATAR_BY_PROVIDER = 72;
protected static final int MATCH_CHATS = 80;
protected static final int MATCH_CHATS_BY_ACCOUNT = 81;
protected static final int MATCH_CHATS_ID = 82;
protected static final int MATCH_SESSIONS = 83;
protected static final int MATCH_SESSIONS_BY_PROVIDER = 84;
protected static final int MATCH_PROVIDER_SETTINGS = 90;
protected static final int MATCH_PROVIDER_SETTINGS_BY_ID = 91;
protected static final int MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME = 92;
protected static final int MATCH_INVITATIONS = 100;
protected static final int MATCH_INVITATION = 101;
protected static final int MATCH_ACCOUNTS_STATUS = 104;
protected static final int MATCH_ACCOUNT_STATUS = 105;
protected static final int MATCH_BRANDING_RESOURCE_MAP_CACHE = 106;
// mcs url matcher
protected static final int MATCH_OUTGOING_RMQ_MESSAGES = 200;
protected static final int MATCH_OUTGOING_RMQ_MESSAGE = 201;
protected static final int MATCH_OUTGOING_HIGHEST_RMQ_ID = 202;
protected static final int MATCH_LAST_RMQ_ID = 203;
protected static final int MATCH_S2D_RMQ_IDS = 204;
protected final UriMatcher mUrlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private String mTransientDbName;
private static final HashMap<String, String> sProviderAccountsProjectionMap;
private static final HashMap<String, String> sAccountsByDomainProjectionMap;
private static final HashMap<String, String> sContactsProjectionMap;
private static final HashMap<String, String> sContactListProjectionMap;
private static final HashMap<String, String> sBlockedListProjectionMap;
private static final HashMap<String, String> sMessagesProjectionMap;
private static final HashMap<String, String> sInMemoryMessagesProjectionMap;
private static final String PROVIDER_JOIN_ACCOUNT_TABLE = "providers LEFT OUTER JOIN accounts ON "
+ "(providers._id = accounts.provider AND accounts.active = 1) "
+ "LEFT OUTER JOIN accountStatus ON (accounts._id = accountStatus.account)";
private static final String DOMAIN_JOIN_ACCOUNT_TABLE = "providerSettings JOIN accounts ON "
+ "(providerSettings.provider = accounts.provider AND providerSettings.name = '" + Imps.ProviderSettings.DOMAIN + "' AND accounts.active = 1) "
+ "LEFT OUTER JOIN accountStatus ON (accounts._id = accountStatus.account)";
private static final String CONTACT_JOIN_PRESENCE_TABLE = "contacts LEFT OUTER JOIN presence ON (contacts._id = presence.contact_id)";
private static final String CONTACT_JOIN_PRESENCE_CHAT_TABLE = CONTACT_JOIN_PRESENCE_TABLE
+ " LEFT OUTER JOIN chats ON (contacts._id = chats.contact_id)";
private static final String CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE = CONTACT_JOIN_PRESENCE_CHAT_TABLE
+ " LEFT OUTER JOIN avatars ON (contacts.username = avatars.contact"
+ " AND contacts.account = avatars.account_id)";
private static final String BLOCKEDLIST_JOIN_AVATAR_TABLE = "blockedList LEFT OUTER JOIN avatars ON (blockedList.username = avatars.contact"
+ " AND blockedList.account = avatars.account_id)";
private static final String MESSAGE_JOIN_CONTACT_TABLE = "messages LEFT OUTER JOIN contacts ON (contacts._id = messages.thread_id)";
private static final String IN_MEMORY_MESSAGES_JOIN_CONTACT_TABLE = "inMemoryMessages LEFT OUTER JOIN contacts ON "
+ "(contacts._id = inMemoryMessages.thread_id)";
/** The where clause for filtering out blocked contacts */
private static final String NON_BLOCKED_CONTACTS_WHERE_CLAUSE = "("
+ Imps.Contacts.TYPE
+ " IS NULL OR "
+ Imps.Contacts.TYPE
+ "!="
+ String.valueOf(Imps.Contacts.TYPE_BLOCKED)
+ ")";
private static final String BLOCKED_CONTACTS_WHERE_CLAUSE = "(contacts." + Imps.Contacts.TYPE
+ "=" + Imps.Contacts.TYPE_BLOCKED
+ ")";
private static final String CONTACT_ID = TABLE_CONTACTS + '.' + Imps.Contacts._ID;
private static final String PRESENCE_CONTACT_ID = TABLE_PRESENCE + '.'
+ Imps.Presence.CONTACT_ID;
protected static DatabaseHelper mDbHelper;
private String mDatabaseName;
private final int mDatabaseVersion;
private final String[] BACKFILL_PROJECTION = { Imps.Chats._ID, Imps.Chats.SHORTCUT,
Imps.Chats.LAST_MESSAGE_DATE };
private final String[] FIND_SHORTCUT_PROJECTION = { Imps.Chats._ID, Imps.Chats.SHORTCUT };
// contact id query projection
private static final String[] CONTACT_ID_PROJECTION = new String[] { Imps.Contacts._ID, // 0
};
private static final int CONTACT_ID_COLUMN = 0;
// contact id query selection for "seed presence" operation
private static final String CONTACTS_WITH_NO_PRESENCE_SELECTION = Imps.Contacts.ACCOUNT + "=?"
+ " AND " + Imps.Contacts._ID
+ " in (select " + CONTACT_ID
+ " from " + TABLE_CONTACTS
+ " left outer join "
+ TABLE_PRESENCE + " on "
+ CONTACT_ID + '='
+ PRESENCE_CONTACT_ID
+ " where "
+ PRESENCE_CONTACT_ID
+ " IS NULL)";
// contact id query selection args 1
private String[] mQueryContactIdSelectionArgs1 = new String[1];
// contact id query selection for getContactId()
private static final String CONTACT_ID_QUERY_SELECTION = Imps.Contacts.ACCOUNT + "=? AND "
+ Imps.Contacts.USERNAME + "=?";
// contact id query selection args 2
private String[] mQueryContactIdSelectionArgs2 = new String[2];
private class DatabaseHelper extends SQLiteOpenHelper {
private SQLiteDatabase dbRead;
private SQLiteDatabase dbWrite;
boolean mInMemoryDB = false;
String mKey = null;
boolean doCleanup = false;
DatabaseHelper(Context context, String key, boolean inMemoryDb) throws Exception {
super(context, mDatabaseName, null, mDatabaseVersion);
mInMemoryDB = inMemoryDb;
mKey = key;
}
public SQLiteDatabase getReadableDatabase() {
if (dbRead == null)
dbRead = super.getReadableDatabase(mKey);
return dbRead;
}
public SQLiteDatabase getWritableDatabase() {
if (dbWrite == null)
dbWrite = super.getWritableDatabase(mKey);
/*
if (doCleanup)
{
//clean up orphaned contacts
dbWrite.execSQL("DELETE FROM " + TABLE_CONTACTS + " WHERE " + TABLE_CONTACTS + '.' + Contacts._ID +
" IN (Select " + TABLE_CONTACTS + '.' + Contacts._ID + " from " + TABLE_CONTACTS + " LEFT JOIN " + TABLE_PROVIDERS + " ON " + Contacts.PROVIDER + "=" + TABLE_PROVIDERS + "." + Provider._ID + " WHERE " + TABLE_PROVIDERS + '.' + Provider._ID + " IS NULL)");
dbWrite.execSQL("DELETE FROM " + TABLE_CONTACTS + " WHERE " + TABLE_CONTACTS + '.' + Contacts._ID + " NOT IN ("
+ "SELECT min(" + Contacts._ID + ") FROM " + TABLE_CONTACTS + " group by " + Contacts.USERNAME + "," + Contacts.PROVIDER + "," + Contacts.ACCOUNT
+ ")");
doCleanup = false;
}
*/
return dbWrite;
}
@Override
public void onCreate(SQLiteDatabase db) {
log("DatabaseHelper.onCreate");
db.execSQL("CREATE TABLE " + TABLE_PROVIDERS + " (" + "_id INTEGER PRIMARY KEY,"
+ "name TEXT," + // eg AIM
"fullname TEXT," + // eg AOL Instance Messenger
"category TEXT," + // a category used for forming intent
"signup_url TEXT" + // web url to visit to create a new account
");");
db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " (" + "_id INTEGER PRIMARY KEY,"
+ "name TEXT," + "provider INTEGER," + "username TEXT," + "pw TEXT,"
+ "active INTEGER NOT NULL DEFAULT 0,"
+ "locked INTEGER NOT NULL DEFAULT 0,"
+ "keep_signed_in INTEGER NOT NULL DEFAULT 0,"
+ "last_login_state INTEGER NOT NULL DEFAULT 0,"
+ "UNIQUE (provider, username)" + ");");
createContactsTables(db);
createMessageChatTables(db, true /* create show_ts column */);
db.execSQL("CREATE TABLE " + TABLE_AVATARS + " (" + "_id INTEGER PRIMARY KEY,"
+ "contact TEXT," + "provider_id INTEGER," + "account_id INTEGER,"
+ "hash TEXT," + "data BLOB," + // raw image data
"UNIQUE (account_id, contact)" + ");");
db.execSQL("CREATE TABLE " + TABLE_PROVIDER_SETTINGS + " ("
+ "_id INTEGER PRIMARY KEY," + "provider INTEGER," + "name TEXT,"
+ "value TEXT," + "UNIQUE (provider, name)" + ");");
db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " ("
+ "_id INTEGER PRIMARY KEY," + "provider_id INTEGER,"
+ "app_res_id INTEGER," + "plugin_res_id INTEGER" + ");");
// clean up account specific data when an account is deleted.
db.execSQL("CREATE TRIGGER account_cleanup " + "DELETE ON " + TABLE_ACCOUNTS
+ " BEGIN " + "DELETE FROM " + TABLE_AVATARS + " WHERE account_id= OLD._id;"
+ "END");
// add a database trigger to clean up associated provider settings
// while deleting a provider
db.execSQL("CREATE TRIGGER provider_cleanup " + "DELETE ON " + TABLE_PROVIDERS
+ " BEGIN " + "DELETE FROM " + TABLE_PROVIDER_SETTINGS
+ " WHERE provider= OLD._id;" + "END");
// the following are tables for mcs
db.execSQL("create TABLE " + TABLE_OUTGOING_RMQ_MESSAGES + " ("
+ "_id INTEGER PRIMARY KEY," + "rmq_id INTEGER," + "type INTEGER,"
+ "ts INTEGER," + "data TEXT" + ");");
db.execSQL("create TABLE " + TABLE_LAST_RMQ_ID + " (" + "_id INTEGER PRIMARY KEY,"
+ "rmq_id INTEGER" + ");");
db.execSQL("create TABLE " + TABLE_S2D_RMQ_IDS + " (" + "_id INTEGER PRIMARY KEY,"
+ "rmq_id INTEGER" + ");");
//DELETE FROM cache WHERE id IN (SELECT cache.id FROM cache LEFT JOIN main ON cache.id=main.id WHERE main.id IS NULL);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LogCleaner.debug(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
switch (oldVersion) {
case 43: // this is the db version shipped in Dream 1.0
// no-op: no schema changed from 43 to 44. The db version was changed to flush
// old provider settings, so new provider setting (including new name/value
// pairs) could be inserted by the plugins.
// follow thru.
case 44:
if (newVersion <= 44) {
return;
}
db.beginTransaction();
try {
// add category column to the providers table
db.execSQL("ALTER TABLE " + TABLE_PROVIDERS + " ADD COLUMN category TEXT;");
// add otr column to the contacts table
db.execSQL("ALTER TABLE " + TABLE_CONTACTS + " ADD COLUMN otr INTEGER;");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
case 45:
if (newVersion <= 45) {
return;
}
db.beginTransaction();
try {
// add an otr_etag column to contact etag table
db.execSQL("ALTER TABLE " + TABLE_CONTACTS_ETAG + " ADD COLUMN otr_etag TEXT;");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
case 46:
if (newVersion <= 46) {
return;
}
db.beginTransaction();
try {
// add branding resource map cache table
db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " ("
+ "_id INTEGER PRIMARY KEY," + "provider_id INTEGER,"
+ "app_res_id INTEGER," + "plugin_res_id INTEGER" + ");");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
case 47:
if (newVersion <= 47) {
return;
}
db.beginTransaction();
try {
// when upgrading from version 47, don't create the show_ts column
// here. The upgrade step in 51 will add the show_ts column to the
// messages table. If we created the messages table with show_ts here,
// we'll get a duplicate column error later.
createMessageChatTables(db, false /* don't create show_ts column */);
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
// fall thru.
case 48:
case 49:
case 50:
if (newVersion <= 50) {
return;
}
db.beginTransaction();
try {
// add rmq2 s2d ids table
db.execSQL("create TABLE " + TABLE_S2D_RMQ_IDS + " ("
+ "_id INTEGER PRIMARY KEY," + "rmq_id INTEGER" + ");");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
case 51:
if (newVersion <= 51) {
return;
}
db.beginTransaction();
try {
db.execSQL("ALTER TABLE " + TABLE_MESSAGES + " ADD COLUMN show_ts INTEGER;");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
return;
case 101:
// This was a no-op upgrade when we added the encrypted DB option
return;
case 103:
try {
db.beginTransaction();
Cursor c = db.query(TABLE_MESSAGES, null, null, null, null, null, null);
if (c.getColumnIndex("mime_type")==-1)
{
db.execSQL("ALTER TABLE " + TABLE_MESSAGES
+ " ADD COLUMN mime_type TEXT;");
}
c.close();
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
} finally {
db.endTransaction();
}
if (mInMemoryDB) { //this should actually be if mInMemoryDB = true, then update the table
try {
db.beginTransaction();
db.execSQL("ALTER TABLE " + TABLE_IN_MEMORY_MESSAGES
+ " ADD COLUMN mime_type TEXT;");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
} finally {
db.endTransaction();
}
}
return;
case 104:
db.rawExecSQL("PRAGMA cipher_migrate;");
return;
case 1:
if (newVersion <= 100) {
return;
}
db.beginTransaction();
try {
db.execSQL("ALTER TABLE " + TABLE_MESSAGES
+ " ADD COLUMN is_delivered INTEGER;");
db.setTransactionSuccessful();
} catch (Throwable ex) {
LogCleaner.error(LOG_TAG, ex.getMessage(), ex);
} finally {
db.endTransaction();
}
return;
}
LogCleaner.warn(LOG_TAG, "Couldn't upgrade db to " + newVersion + ". Destroying old data.");
destroyOldTables(db);
onCreate(db);
}
private void destroyOldTables(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDERS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_ACCOUNTS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACT_LIST);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BLOCKED_LIST);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS_ETAG);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_AVATARS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDER_SETTINGS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BRANDING_RESOURCE_MAP_CACHE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_MESSAGES);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_CHATS);
// mcs/rmq stuff
db.execSQL("DROP TABLE IF EXISTS " + TABLE_OUTGOING_RMQ_MESSAGES);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_RMQ_ID);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_S2D_RMQ_IDS);
}
private void createContactsTables(SQLiteDatabase db) {
log("createContactsTables");
StringBuilder buf = new StringBuilder();
String contactsTableName = TABLE_CONTACTS;
// creating the "contacts" table
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(contactsTableName);
buf.append(" (");
buf.append("_id INTEGER PRIMARY KEY,");
buf.append("username TEXT,");
buf.append("nickname TEXT,");
buf.append("provider INTEGER,");
buf.append("account INTEGER,");
buf.append("contactList INTEGER,");
buf.append("type INTEGER,");
buf.append("subscriptionStatus INTEGER,");
buf.append("subscriptionType INTEGER,");
// the following are derived from Google Contact Extension, we don't include all
// the attributes, just the ones we can use.
// (see http://code.google.com/apis/talk/jep_extensions/roster_attributes.html)
//
// qc: quick contact (derived from message count)
// rejected: if the contact has ever been rejected by the user
buf.append("qc INTEGER,");
buf.append("rejected INTEGER,");
// Off the record status
buf.append("otr INTEGER");
buf.append(");");
db.execSQL(buf.toString());
buf.delete(0, buf.length());
// creating contact etag table
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(TABLE_CONTACTS_ETAG);
buf.append(" (");
buf.append("_id INTEGER PRIMARY KEY,");
buf.append("etag TEXT,");
buf.append("otr_etag TEXT,");
buf.append("account INTEGER UNIQUE");
buf.append(");");
db.execSQL(buf.toString());
buf.delete(0, buf.length());
// creating the "contactList" table
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(TABLE_CONTACT_LIST);
buf.append(" (");
buf.append("_id INTEGER PRIMARY KEY,");
buf.append("name TEXT,");
buf.append("provider INTEGER,");
buf.append("account INTEGER");
buf.append(");");
db.execSQL(buf.toString());
buf.delete(0, buf.length());
// creating the "blockedList" table
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(TABLE_BLOCKED_LIST);
buf.append(" (");
buf.append("_id INTEGER PRIMARY KEY,");
buf.append("username TEXT,");
buf.append("nickname TEXT,");
buf.append("provider INTEGER,");
buf.append("account INTEGER");
buf.append(");");
db.execSQL(buf.toString());
}
private void createMessageChatTables(SQLiteDatabase db,
boolean addShowTsColumnForMessagesTable) {
log("createMessageChatTables");
// message table
StringBuilder buf = new StringBuilder();
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(TABLE_MESSAGES);
buf.append(" (_id INTEGER PRIMARY KEY,");
buf.append("thread_id INTEGER,");
buf.append("nickname TEXT,");
buf.append("body TEXT,");
buf.append("date INTEGER,");
buf.append("type INTEGER,");
buf.append("packet_id TEXT UNIQUE,");
buf.append("err_code INTEGER NOT NULL DEFAULT 0,");
buf.append("err_msg TEXT,");
buf.append("is_muc INTEGER");
if (addShowTsColumnForMessagesTable) {
buf.append(",show_ts INTEGER");
}
buf.append(",is_delivered INTEGER");
buf.append(",mime_type TEXT");
buf.append(");");
String sqlStatement = buf.toString();
log("create message table: " + sqlStatement);
db.execSQL(sqlStatement);
buf.delete(0, buf.length());
buf.append("CREATE TABLE IF NOT EXISTS ");
buf.append(TABLE_CHATS);
buf.append(" (_id INTEGER PRIMARY KEY,");
buf.append("contact_id INTEGER UNIQUE,");
buf.append("jid_resource TEXT,"); // the JID resource for the user, for non-group chats
buf.append("groupchat INTEGER,"); // 1 if group chat, 0 if not TODO: remove this column
buf.append("last_unread_message TEXT,"); // the last unread message
buf.append("last_message_date INTEGER,"); // in seconds
buf.append("unsent_composed_message TEXT,"); // a composed, but not sent message
buf.append("shortcut INTEGER);"); // which of 10 slots (if any) this chat occupies
// chat sessions, including single person chats and group chats
sqlStatement = buf.toString();
log("create chat table: " + sqlStatement);
db.execSQL(sqlStatement);
buf.delete(0, buf.length());
buf.append("CREATE TRIGGER IF NOT EXISTS contact_cleanup ");
buf.append("DELETE ON contacts ");
buf.append("BEGIN ");
buf.append("DELETE FROM ").append(TABLE_CHATS).append(" WHERE contact_id = OLD._id;");
buf.append("DELETE FROM ").append(TABLE_MESSAGES).append(" WHERE thread_id = OLD._id;");
buf.append("END");
sqlStatement = buf.toString();
log("create trigger: " + sqlStatement);
db.execSQL(sqlStatement);
}
private void createInMemoryMessageTables(SQLiteDatabase db, String tablePrefix) {
String tableName = (tablePrefix != null) ? tablePrefix + TABLE_IN_MEMORY_MESSAGES
: TABLE_IN_MEMORY_MESSAGES;
db.execSQL("CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ "_id INTEGER PRIMARY KEY," + "thread_id INTEGER," + "nickname TEXT,"
+ "body TEXT,"
+ "date INTEGER,"
+ // in millisec
"type INTEGER," + "packet_id TEXT UNIQUE,"
+ "err_code INTEGER NOT NULL DEFAULT 0," + "err_msg TEXT,"
+ "is_muc INTEGER," + "show_ts INTEGER," +
"is_delivered INTEGER," +
"mime_type TEXT" +
");");
}
@Override
public void onOpen(SQLiteDatabase db) {
if (db.isReadOnly()) {
LogCleaner.warn(LOG_TAG, "ImProvider database opened in read only mode.");
LogCleaner.warn(LOG_TAG, "Transient tables not created.");
return;
}
log("##### createTransientTables");
// Create transient tables
String cpDbName;
if (mInMemoryDB)
{
db.execSQL("ATTACH DATABASE ':memory:' AS " + mTransientDbName + ";");
cpDbName = mTransientDbName + ".";
}
else
{
cpDbName = "";
}
// in-memory message table
createInMemoryMessageTables(db, cpDbName);
// presence
db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_PRESENCE + " ("
+ "_id INTEGER PRIMARY KEY," + "contact_id INTEGER UNIQUE,"
+ "jid_resource TEXT," + // jid resource for the presence
"client_type INTEGER," + // client type
"priority INTEGER," + // presence priority (XMPP)
"mode INTEGER," + // presence mode
"status TEXT" + // custom status
");");
// group chat invitations
db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_INVITATIONS + " ("
+ "_id INTEGER PRIMARY KEY," + "providerId INTEGER," + "accountId INTEGER,"
+ "inviteId TEXT," + "sender TEXT," + "groupName TEXT," + "note TEXT,"
+ "status INTEGER" + ");");
// group chat members
db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_GROUP_MEMBERS + " ("
+ "_id INTEGER PRIMARY KEY," + "groupId INTEGER," + "username TEXT,"
+ "nickname TEXT" + ");");
db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_ACCOUNT_STATUS + " ("
+ "_id INTEGER PRIMARY KEY," + "account INTEGER UNIQUE,"
+ "presenceStatus INTEGER," + "connStatus INTEGER" + ");");
/* when we moved the contact table out of transient_db and into the main db, the
presence and groupchat cleanup triggers don't work anymore. It seems we can't
create triggers that reference objects in a different database!
// Insert a default presence for newly inserted contact
db.execSQL("CREATE TRIGGER IF NOT EXISTS contact_create_presence " +
"AFTER INSERT ON " + contactsTableName +
" WHEN NEW.type != " + Im.Contacts.TYPE_GROUP +
" BEGIN " +
"INSERT INTO presence (contact_id) VALUES (NEW._id);" +
" END");
// Remove the presence when the contact is removed.
db.execSQL("CREATE TRIGGER IF NOT EXISTS contact_presence_cleanup " +
"DELETE ON " + contactsTableName +
" BEGIN " +
"DELETE FROM presence WHERE contact_id = OLD._id;" +
"END");
// Cleans up group members and group messages when a group chat is deleted
db.execSQL("CREATE TRIGGER IF NOT EXISTS " + cpDbName + "group_cleanup " +
"DELETE ON " + cpDbName + contactsTableName +
" FOR EACH ROW WHEN OLD.type = " + Im.Contacts.TYPE_GROUP +
" BEGIN " +
"DELETE FROM groupMembers WHERE groupId = OLD._id;" +
"DELETE FROM groupMessages WHERE groupId = OLD._id;" +
" END");
*/
// only store the session cookies in memory right now. This means
// that we don't persist them across device reboot
db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_SESSION_COOKIES + " ("
+ "_id INTEGER PRIMARY KEY," + "provider INTEGER," + "account INTEGER,"
+ "name TEXT," + "value TEXT" + ");");
}
@Override
public synchronized void close() {
if (dbRead != null && dbRead.isOpen())
dbRead.close();
if (dbWrite != null && dbWrite.isOpen())
dbWrite.close();
super.close();
}
}
static {
sProviderAccountsProjectionMap = new HashMap<String, String>();
sProviderAccountsProjectionMap.put(Imps.Provider._ID, "providers._id AS _id");
sProviderAccountsProjectionMap.put(Imps.Provider._COUNT, "COUNT(*) AS _account");
sProviderAccountsProjectionMap.put(Imps.Provider.NAME, "providers.name AS name");
sProviderAccountsProjectionMap
.put(Imps.Provider.FULLNAME, "providers.fullname AS fullname");
sProviderAccountsProjectionMap
.put(Imps.Provider.CATEGORY, "providers.category AS category");
sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_ID,
"accounts._id AS account_id");
sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_USERNAME,
"accounts.username AS account_username");
sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_PW,
"accounts.pw AS account_pw");
sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_LOCKED,
"accounts.locked AS account_locked");
sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_KEEP_SIGNED_IN,
"accounts.keep_signed_in AS account_keepSignedIn");
sProviderAccountsProjectionMap.put(Imps.Provider.ACCOUNT_PRESENCE_STATUS,
"accountStatus.presenceStatus AS account_presenceStatus");
sProviderAccountsProjectionMap.put(Imps.Provider.ACCOUNT_CONNECTION_STATUS,
"accountStatus.connStatus AS account_connStatus");
sAccountsByDomainProjectionMap = new HashMap<String, String>();
sAccountsByDomainProjectionMap.put(Imps.Account._ID, "accounts._id AS _id");
// contacts projection map
sContactsProjectionMap = new HashMap<String, String>();
// Base column
sContactsProjectionMap.put(Imps.Contacts._ID, "contacts._id AS _id");
sContactsProjectionMap.put(Imps.Contacts._COUNT, "COUNT(*) AS _count");
// contacts column
sContactsProjectionMap.put(Imps.Contacts._ID, "contacts._id as _id");
sContactsProjectionMap.put(Imps.Contacts.USERNAME, "contacts.username as username");
sContactsProjectionMap.put(Imps.Contacts.NICKNAME, "contacts.nickname as nickname");
sContactsProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider as provider");
sContactsProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account as account");
sContactsProjectionMap
.put(Imps.Contacts.CONTACTLIST, "contacts.contactList as contactList");
sContactsProjectionMap.put(Imps.Contacts.TYPE, "contacts.type as type");
sContactsProjectionMap.put(Imps.Contacts.SUBSCRIPTION_STATUS,
"contacts.subscriptionStatus as subscriptionStatus");
sContactsProjectionMap.put(Imps.Contacts.SUBSCRIPTION_TYPE,
"contacts.subscriptionType as subscriptionType");
sContactsProjectionMap.put(Imps.Contacts.QUICK_CONTACT, "contacts.qc as qc");
sContactsProjectionMap.put(Imps.Contacts.REJECTED, "contacts.rejected as rejected");
// Presence columns
sContactsProjectionMap.put(Imps.Presence.CONTACT_ID, "presence.contact_id AS contact_id");
sContactsProjectionMap.put(Imps.Contacts.PRESENCE_STATUS, "presence.mode AS mode");
sContactsProjectionMap.put(Imps.Contacts.PRESENCE_CUSTOM_STATUS,
"presence.status AS status");
sContactsProjectionMap
.put(Imps.Contacts.CLIENT_TYPE, "presence.client_type AS client_type");
// Chats columns
sContactsProjectionMap.put(Imps.Contacts.CHATS_CONTACT,
"chats.contact_id AS chats_contact_id");
sContactsProjectionMap.put(Imps.Chats.JID_RESOURCE, "chats.jid_resource AS jid_resource");
sContactsProjectionMap.put(Imps.Chats.GROUP_CHAT, "chats.groupchat AS groupchat");
sContactsProjectionMap.put(Imps.Contacts.LAST_UNREAD_MESSAGE,
"chats.last_unread_message AS last_unread_message");
sContactsProjectionMap.put(Imps.Contacts.LAST_MESSAGE_DATE,
"chats.last_message_date AS last_message_date");
sContactsProjectionMap.put(Imps.Contacts.UNSENT_COMPOSED_MESSAGE,
"chats.unsent_composed_message AS unsent_composed_message");
sContactsProjectionMap.put(Imps.Contacts.SHORTCUT, "chats.SHORTCUT AS shortcut");
// Avatars columns
sContactsProjectionMap.put(Imps.Contacts.AVATAR_HASH, "avatars.hash AS avatars_hash");
sContactsProjectionMap.put(Imps.Contacts.AVATAR_DATA, "quote(avatars.data) AS avatars_data");
// contactList projection map
sContactListProjectionMap = new HashMap<String, String>();
sContactListProjectionMap.put(Imps.ContactList._ID, "contactList._id AS _id");
sContactListProjectionMap.put(Imps.ContactList._COUNT, "COUNT(*) AS _count");
sContactListProjectionMap.put(Imps.ContactList.NAME, "name");
sContactListProjectionMap.put(Imps.ContactList.PROVIDER, "provider");
sContactListProjectionMap.put(Imps.ContactList.ACCOUNT, "account");
// blockedList projection map
sBlockedListProjectionMap = new HashMap<String, String>();
sBlockedListProjectionMap.put(Imps.BlockedList._ID, "blockedList._id AS _id");
sBlockedListProjectionMap.put(Imps.BlockedList._COUNT, "COUNT(*) AS _count");
sBlockedListProjectionMap.put(Imps.BlockedList.USERNAME, "username");
sBlockedListProjectionMap.put(Imps.BlockedList.NICKNAME, "nickname");
sBlockedListProjectionMap.put(Imps.BlockedList.PROVIDER, "provider");
sBlockedListProjectionMap.put(Imps.BlockedList.ACCOUNT, "account");
sBlockedListProjectionMap.put(Imps.BlockedList.AVATAR_DATA, "quote(avatars.data) AS avatars_data");
// messages projection map
sMessagesProjectionMap = new HashMap<String, String>();
sMessagesProjectionMap.put(Imps.Messages._ID, "messages._id AS _id");
sMessagesProjectionMap.put(Imps.Messages._COUNT, "COUNT(*) AS _count");
sMessagesProjectionMap.put(Imps.Messages.THREAD_ID, "messages.thread_id AS thread_id");
sMessagesProjectionMap.put(Imps.Messages.PACKET_ID, "messages.packet_id AS packet_id");
sMessagesProjectionMap.put(Imps.Messages.NICKNAME, "messages.nickname AS nickname");
sMessagesProjectionMap.put(Imps.Messages.BODY, "messages.body AS body");
sMessagesProjectionMap.put(Imps.Messages.DATE, "messages.date AS date");
sMessagesProjectionMap.put(Imps.Messages.TYPE, "messages.type AS type");
sMessagesProjectionMap.put(Imps.Messages.ERROR_CODE, "messages.err_code AS err_code");
sMessagesProjectionMap.put(Imps.Messages.ERROR_MESSAGE, "messages.err_msg AS err_msg");
sMessagesProjectionMap.put(Imps.Messages.IS_GROUP_CHAT, "messages.is_muc AS is_muc");
sMessagesProjectionMap.put(Imps.Messages.DISPLAY_SENT_TIME, "messages.show_ts AS show_ts");
sMessagesProjectionMap.put(Imps.Messages.IS_DELIVERED,
"messages.is_delivered AS is_delivered");
sMessagesProjectionMap.put(Imps.Messages.MIME_TYPE,
"messages.mime_type AS mime_type");
// contacts columns
sMessagesProjectionMap.put(Imps.Messages.CONTACT, "contacts.username AS contact");
sMessagesProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider AS provider");
sMessagesProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account AS account");
sMessagesProjectionMap.put("contact_type", "contacts.type AS contact_type");
sInMemoryMessagesProjectionMap = new HashMap<String, String>();
sInMemoryMessagesProjectionMap.put(Imps.Messages._ID, "inMemoryMessages._id AS _id");
sInMemoryMessagesProjectionMap.put(Imps.Messages._COUNT, "COUNT(*) AS _count");
sInMemoryMessagesProjectionMap.put(Imps.Messages.THREAD_ID,
"inMemoryMessages.thread_id AS thread_id");
sInMemoryMessagesProjectionMap.put(Imps.Messages.PACKET_ID,
"inMemoryMessages.packet_id AS packet_id");
sInMemoryMessagesProjectionMap.put(Imps.Messages.NICKNAME,
"inMemoryMessages.nickname AS nickname");
sInMemoryMessagesProjectionMap.put(Imps.Messages.BODY, "inMemoryMessages.body AS body");
sInMemoryMessagesProjectionMap.put(Imps.Messages.DATE, "inMemoryMessages.date AS date");
sInMemoryMessagesProjectionMap.put(Imps.Messages.TYPE, "inMemoryMessages.type AS type");
sInMemoryMessagesProjectionMap.put(Imps.Messages.ERROR_CODE,
"inMemoryMessages.err_code AS err_code");
sInMemoryMessagesProjectionMap.put(Imps.Messages.ERROR_MESSAGE,
"inMemoryMessages.err_msg AS err_msg");
sInMemoryMessagesProjectionMap.put(Imps.Messages.IS_GROUP_CHAT,
"inMemoryMessages.is_muc AS is_muc");
sInMemoryMessagesProjectionMap.put(Imps.Messages.DISPLAY_SENT_TIME,
"inMemoryMessages.show_ts AS show_ts");
sInMemoryMessagesProjectionMap.put(Imps.Messages.IS_DELIVERED,
"inMemoryMessages.is_delivered AS is_delivered");
sInMemoryMessagesProjectionMap.put(Imps.Messages.MIME_TYPE,
"inMemoryMessages.mime_type AS mime_type");
// contacts columns
sInMemoryMessagesProjectionMap.put(Imps.Messages.CONTACT, "contacts.username AS contact");
sInMemoryMessagesProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider AS provider");
sInMemoryMessagesProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account AS account");
sInMemoryMessagesProjectionMap.put("contact_type", "contacts.type AS contact_type");
}
public ImpsProvider() {
this(DATABASE_VERSION);
setupImUrlMatchers(AUTHORITY);
setupMcsUrlMatchers(AUTHORITY);
}
protected ImpsProvider(int dbVersion) {
mDatabaseVersion = dbVersion;
}
private void setupImUrlMatchers(String authority) {
mUrlMatcher.addURI(authority, "providers", MATCH_PROVIDERS);
mUrlMatcher.addURI(authority, "providers/#", MATCH_PROVIDERS_BY_ID);
mUrlMatcher.addURI(authority, "providers/account", MATCH_PROVIDERS_WITH_ACCOUNT);
mUrlMatcher.addURI(authority, "accounts", MATCH_ACCOUNTS);
mUrlMatcher.addURI(authority, "domainAccounts", MATCH_ACCOUNTS_WITH_DOMAIN);
mUrlMatcher.addURI(authority, "accounts/#", MATCH_ACCOUNTS_BY_ID);
mUrlMatcher.addURI(authority, "contacts", MATCH_CONTACTS);
mUrlMatcher.addURI(authority, "contactsWithPresence", MATCH_CONTACTS_JOIN_PRESENCE);
mUrlMatcher.addURI(authority, "contactsBarebone", MATCH_CONTACTS_BAREBONE);
mUrlMatcher.addURI(authority, "contacts/#/#", MATCH_CONTACTS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contacts/chatting", MATCH_CHATTING_CONTACTS);
mUrlMatcher.addURI(authority, "contacts/chatting/#/#", MATCH_CHATTING_CONTACTS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contacts/online/#/#", MATCH_ONLINE_CONTACTS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contacts/offline/#/#", MATCH_OFFLINE_CONTACTS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contacts/#", MATCH_CONTACT);
mUrlMatcher.addURI(authority, "contacts/blocked", MATCH_BLOCKED_CONTACTS);
mUrlMatcher.addURI(authority, "bulk_contacts", MATCH_CONTACTS_BULK);
mUrlMatcher.addURI(authority, "contacts/onlineCount", MATCH_ONLINE_CONTACT_COUNT);
mUrlMatcher.addURI(authority, "contactLists", MATCH_CONTACTLISTS);
mUrlMatcher.addURI(authority, "contactLists/#/#", MATCH_CONTACTLISTS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contactLists/#", MATCH_CONTACTLIST);
mUrlMatcher.addURI(authority, "blockedList", MATCH_BLOCKEDLIST);
mUrlMatcher.addURI(authority, "blockedList/#/#", MATCH_BLOCKEDLIST_BY_PROVIDER);
mUrlMatcher.addURI(authority, "contactsEtag", MATCH_CONTACTS_ETAGS);
mUrlMatcher.addURI(authority, "contactsEtag/#", MATCH_CONTACTS_ETAG);
mUrlMatcher.addURI(authority, "presence", MATCH_PRESENCE);
mUrlMatcher.addURI(authority, "presence/#", MATCH_PRESENCE_ID);
mUrlMatcher.addURI(authority, "presence/account/#", MATCH_PRESENCE_BY_ACCOUNT);
mUrlMatcher.addURI(authority, "seed_presence/account/#", MATCH_PRESENCE_SEED_BY_ACCOUNT);
mUrlMatcher.addURI(authority, "bulk_presence", MATCH_PRESENCE_BULK);
mUrlMatcher.addURI(authority, "messages", MATCH_MESSAGES);
mUrlMatcher.addURI(authority, "messagesByAcctAndContact/#/*", MATCH_MESSAGES_BY_CONTACT);
mUrlMatcher.addURI(authority, "messagesByThreadId/#", MATCH_MESSAGES_BY_THREAD_ID);
mUrlMatcher.addURI(authority, "messagesByProvider/#", MATCH_MESSAGES_BY_PROVIDER);
mUrlMatcher.addURI(authority, "messagesByAccount/#", MATCH_MESSAGES_BY_ACCOUNT);
mUrlMatcher.addURI(authority, "messages/#", MATCH_MESSAGE);
mUrlMatcher.addURI(authority, "messagesByPacketId/*", MATCH_MESSAGES_BY_PACKET_ID);
mUrlMatcher.addURI(authority, "otrMessages", MATCH_OTR_MESSAGES);
mUrlMatcher.addURI(authority, "otrMessagesByAcctAndContact/#/*",
MATCH_OTR_MESSAGES_BY_CONTACT);
mUrlMatcher.addURI(authority, "otrMessagesByThreadId/#", MATCH_OTR_MESSAGES_BY_THREAD_ID);
mUrlMatcher.addURI(authority, "otrMessagesByProvider/#", MATCH_OTR_MESSAGES_BY_PROVIDER);
mUrlMatcher.addURI(authority, "otrMessagesByAccount/#", MATCH_OTR_MESSAGES_BY_ACCOUNT);
mUrlMatcher.addURI(authority, "otrMessagesByPacketId/*", MATCH_OTR_MESSAGES_BY_PACKET_ID);
mUrlMatcher.addURI(authority, "otrMessages/#", MATCH_OTR_MESSAGE);
mUrlMatcher.addURI(authority, "groupMembers", MATCH_GROUP_MEMBERS);
mUrlMatcher.addURI(authority, "groupMembers/#", MATCH_GROUP_MEMBERS_BY_GROUP);
mUrlMatcher.addURI(authority, "avatars", MATCH_AVATARS);
mUrlMatcher.addURI(authority, "avatars/#", MATCH_AVATAR);
mUrlMatcher.addURI(authority, "avatarsBy/#/#", MATCH_AVATAR_BY_PROVIDER);
mUrlMatcher.addURI(authority, "chats", MATCH_CHATS);
mUrlMatcher.addURI(authority, "chats/account/#", MATCH_CHATS_BY_ACCOUNT);
mUrlMatcher.addURI(authority, "chats/#", MATCH_CHATS_ID);
mUrlMatcher.addURI(authority, "sessionCookies", MATCH_SESSIONS);
mUrlMatcher.addURI(authority, "sessionCookiesBy/#/#", MATCH_SESSIONS_BY_PROVIDER);
mUrlMatcher.addURI(authority, "providerSettings", MATCH_PROVIDER_SETTINGS);
mUrlMatcher.addURI(authority, "providerSettings/#", MATCH_PROVIDER_SETTINGS_BY_ID);
mUrlMatcher.addURI(authority, "providerSettings/#/*",
MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME);
mUrlMatcher.addURI(authority, "invitations", MATCH_INVITATIONS);
mUrlMatcher.addURI(authority, "invitations/#", MATCH_INVITATION);
mUrlMatcher.addURI(authority, "accountStatus", MATCH_ACCOUNTS_STATUS);
mUrlMatcher.addURI(authority, "accountStatus/#", MATCH_ACCOUNT_STATUS);
mUrlMatcher.addURI(authority, "brandingResMapCache", MATCH_BRANDING_RESOURCE_MAP_CACHE);
}
private void setupMcsUrlMatchers(String authority) {
mUrlMatcher.addURI(authority, "outgoingRmqMessages", MATCH_OUTGOING_RMQ_MESSAGES);
mUrlMatcher.addURI(authority, "outgoingRmqMessages/#", MATCH_OUTGOING_RMQ_MESSAGE);
mUrlMatcher.addURI(authority, "outgoingHighestRmqId", MATCH_OUTGOING_HIGHEST_RMQ_ID);
mUrlMatcher.addURI(authority, "lastRmqId", MATCH_LAST_RMQ_ID);
mUrlMatcher.addURI(authority, "s2dids", MATCH_S2D_RMQ_IDS);
}
@Override
public boolean onCreate() {
return true;
}
private void setDatabaseName(boolean isEncrypted) {
mDatabaseName = isEncrypted ? ENCRYPTED_DATABASE_NAME : UNENCRYPTED_DATABASE_NAME;
mTransientDbName = "transient_" + mDatabaseName.replace(".", "_");
}
private synchronized DatabaseHelper initDBHelper(String pkey, boolean noCreate) throws Exception {
if (mDbHelper == null) {
if (pkey != null) {
setDatabaseName(!pkey.isEmpty());
Context ctx = getContext();
String path = ctx.getDatabasePath(mDatabaseName).getPath();
if (noCreate && !new File(path).exists()) {
LogCleaner.debug(ImApp.LOG_TAG, "no DB exists at " + path);
return null;
}
boolean inMemoryDb = false;
mDbHelper = new DatabaseHelper(ctx, pkey, inMemoryDb);
LogCleaner.debug(LOG_TAG, "Opened DB with key - empty=" + pkey.isEmpty());
Debug.recordTrail(getContext(), EMPTY_KEY_TRAIL_TAG, "" + pkey.isEmpty());
String prevOpen = Debug.getTrail(getContext(), DATABASE_OPEN_TRAIL_TAG);
if (prevOpen != null) {
Debug.recordTrail(getContext(), PREV_DATABASE_OPEN_TRAIL_TAG, prevOpen);
}
Debug.recordTrail(getContext(), DATABASE_OPEN_TRAIL_TAG, new Date());
} else {
LogCleaner.warn(ImApp.LOG_TAG, "DB not open and no password provided");
}
}
return mDbHelper;
}
private DatabaseHelper getDBHelper() {
return mDbHelper;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public final int update(final Uri url, final ContentValues values, final String selection,
final String[] selectionArgs) {
DatabaseHelper dbHelper = getDBHelper();
int result = 0;
if (dbHelper != null)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
synchronized (db)
{
if (db.isOpen())
{
try {
db.beginTransaction();
result = updateInternal(url, values, selection, selectionArgs);
db.setTransactionSuccessful();
db.endTransaction();
}
catch (Exception e){
if (db.isOpen() && db.inTransaction())
db.endTransaction();
}
if (result > 0) {
getContext().getContentResolver()
.notifyChange(url, null /* observer */, false /* sync */);
}
}
}
}
return result;
}
@Override
public final int delete(final Uri url, final String selection, final String[] selectionArgs) {
int result = -1;
if (getDBHelper() != null)
{
SQLiteDatabase db = getDBHelper().getWritableDatabase();
synchronized (db)
{
if (db.isOpen()) //db can be closed if service sign out takes longer than app/cacheword lock
{
try {
db.beginTransaction();
result = deleteInternal(url, selection, selectionArgs);
db.setTransactionSuccessful();
db.endTransaction();
}
catch (Exception e)
{
//could not delete
}
if (result > 0) {
getContext().getContentResolver()
.notifyChange(url, null /* observer */, false /* sync */);
}
}
}
}
return result;
}
@Override
public final Uri insert(final Uri url, final ContentValues values) {
Uri result = null;
if (getDBHelper() != null)
{
try
{
SQLiteDatabase db = getDBHelper().getWritableDatabase();
synchronized (db)
{
if (db.isOpen())
{
db.beginTransaction();
try {
result = insertInternal(url, values);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (result != null) {
getContext().getContentResolver()
.notifyChange(url, null /* observer */, false /* sync */);
}
}
}
}
catch (IllegalStateException ise)
{
log("database closed when insert attempted: " + url.toString());
}
}
return result;
}
@Override
public final Cursor query(final Uri url, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
return queryInternal(url, projection, selection, selectionArgs, sortOrder);
}
boolean mLoadedLibs = false;
public Cursor queryInternal(Uri url, String[] projectionIn, String selection,
String[] selectionArgs, String sort) {
Debug.onServiceStart();
if (!mLoadedLibs)
{
SQLiteDatabase.loadLibs(this.getContext().getApplicationContext());
mLoadedLibs = true;
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
StringBuilder whereClause = new StringBuilder();
if (selection != null) {
whereClause.append(selection);
}
String groupBy = null;
String limit = null;
String pkey = url.getQueryParameter(ImApp.CACHEWORD_PASSWORD_KEY);
boolean noCreate = "1".equals(url.getQueryParameter(ImApp.NO_CREATE_KEY));
boolean clearKey = "1".equals(url.getQueryParameter(ImApp.CLEAR_PASSWORD_KEY));
if (clearKey) {
if (mDbHelper != null) {
mDbHelper.close();
mDbHelper = null;
}
return null;
}
try {
initDBHelper(pkey, noCreate);
} catch (Exception e) {
LogCleaner.error(ImApp.LOG_TAG, e.getMessage(), e);
return null;
}
if (mDbHelper == null) {
// Failed to open
return null;
}
if (pkey != null)
{
OtrAndroidKeyManagerImpl.setKeyStorePassword(pkey);
}
/*
* String dbKey = null;
if (sort != null && sort.startsWith("key="))
{
if (sort.length() > 4)
{
dbKey = sort.substring(4);
sort = Imps.Provider.DEFAULT_SORT_ORDER;
try
{
initDBHelper(dbKey);
}
catch (Exception se)
{
//passcode must be incorrect or other db issue
return null;
}
}
else
{
return null;
}
}
else
{
return null;
}
*/
// Generate the body of the query
int match = mUrlMatcher.match(url);
{
//log("query " + url + ", match " + match + ", where " + selection);
if (selectionArgs != null) {
for (String selectionArg : selectionArgs) {
log(" selectionArg: " + selectionArg);
}
}
}
switch (match) {
case MATCH_PROVIDERS_BY_ID:
appendWhere(whereClause, Imps.Provider._ID, "=", url.getPathSegments().get(1));
// fall thru.
case MATCH_PROVIDERS:
qb.setTables(TABLE_PROVIDERS);
break;
case MATCH_PROVIDERS_WITH_ACCOUNT:
qb.setTables(PROVIDER_JOIN_ACCOUNT_TABLE);
qb.setProjectionMap(sProviderAccountsProjectionMap);
break;
case MATCH_ACCOUNTS_WITH_DOMAIN:
qb.setTables(DOMAIN_JOIN_ACCOUNT_TABLE);
qb.setProjectionMap(sAccountsByDomainProjectionMap);
break;
case MATCH_ACCOUNTS_BY_ID:
appendWhere(whereClause, Imps.Account._ID, "=", url.getPathSegments().get(1));
// falls down
case MATCH_ACCOUNTS:
qb.setTables(TABLE_ACCOUNTS);
break;
case MATCH_CONTACTS:
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
break;
case MATCH_CONTACTS_JOIN_PRESENCE:
qb.setTables(CONTACT_JOIN_PRESENCE_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
break;
case MATCH_CONTACTS_BAREBONE:
qb.setTables(TABLE_CONTACTS);
break;
case MATCH_CHATTING_CONTACTS:
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
// no need to add the non blocked contacts clause because
// blocked contacts can't have conversations.
break;
case MATCH_CONTACTS_BY_PROVIDER:
buildQueryContactsByProvider(qb, whereClause, url);
appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
break;
case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
buildQueryContactsByProvider(qb, whereClause, url);
appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
// no need to add the non blocked contacts clause because
// blocked contacts can't have conversations.
break;
case MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER:
buildQueryContactsByProvider(qb, whereClause, url);
appendWhere(whereClause, "chats.last_message_date IS NULL");
appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
break;
case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
buildQueryContactsByProvider(qb, whereClause, url);
appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "!=", Imps.Presence.OFFLINE);
appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
break;
case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
buildQueryContactsByProvider(qb, whereClause, url);
appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "=", Imps.Presence.OFFLINE);
appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
break;
case MATCH_BLOCKED_CONTACTS:
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
appendWhere(whereClause, BLOCKED_CONTACTS_WHERE_CLAUSE);
break;
case MATCH_CONTACT:
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
appendWhere(whereClause, "contacts._id", "=", url.getPathSegments().get(1));
break;
case MATCH_ONLINE_CONTACT_COUNT:
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "!=", Imps.Presence.OFFLINE);
appendWhere(whereClause, "chats.last_message_date IS NULL");
appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
groupBy = Imps.Contacts.CONTACTLIST;
break;
case MATCH_CONTACTLISTS_BY_PROVIDER:
appendWhere(whereClause, Imps.ContactList.ACCOUNT, "=", url.getPathSegments().get(2));
// fall through
case MATCH_CONTACTLISTS:
qb.setTables(TABLE_CONTACT_LIST);
qb.setProjectionMap(sContactListProjectionMap);
break;
case MATCH_CONTACTLIST:
qb.setTables(TABLE_CONTACT_LIST);
appendWhere(whereClause, Imps.ContactList._ID, "=", url.getPathSegments().get(1));
break;
case MATCH_BLOCKEDLIST:
qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
qb.setProjectionMap(sBlockedListProjectionMap);
break;
case MATCH_BLOCKEDLIST_BY_PROVIDER:
qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
qb.setProjectionMap(sBlockedListProjectionMap);
appendWhere(whereClause, Imps.BlockedList.ACCOUNT, "=", url.getPathSegments().get(2));
break;
case MATCH_CONTACTS_ETAGS:
qb.setTables(TABLE_CONTACTS_ETAG);
break;
case MATCH_CONTACTS_ETAG:
qb.setTables(TABLE_CONTACTS_ETAG);
appendWhere(whereClause, "_id", "=", url.getPathSegments().get(1));
break;
case MATCH_MESSAGES_BY_THREAD_ID:
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", url.getPathSegments().get(1));
// fall thru.
case MATCH_MESSAGES:
qb.setTables(TABLE_MESSAGES);
final String selectionClause = whereClause.toString();
final String query1 = qb.buildQuery(projectionIn, selectionClause, null, null, null,
null, null /* limit */);
// Build the second query for frequent
qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_IN_MEMORY_MESSAGES);
final String query2 = qb.buildQuery(projectionIn, selectionClause, null, null, null,
null, null /* limit */);
// Put them together
final String query = qb.buildUnionQuery(new String[] { query1, query2 }, sort, null);
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
String[] doubleArgs = null;
if (selectionArgs != null) {
doubleArgs = new String[ selectionArgs.length * 2];//Arrays.copyOf(selectionArgs, selectionArgs.length * 2);
System.arraycopy(selectionArgs, 0, doubleArgs, 0, selectionArgs.length);
System.arraycopy(selectionArgs, 0, doubleArgs, selectionArgs.length, selectionArgs.length);
}
Cursor c = db.rawQueryWithFactory(null, query, doubleArgs, TABLE_MESSAGES);
if ((c != null) && !isTemporary()) {
c.setNotificationUri(getContext().getContentResolver(), url);
}
return c;
case MATCH_MESSAGE:
qb.setTables(TABLE_MESSAGES);
appendWhere(whereClause, Imps.Messages._ID, "=", url.getPathSegments().get(1));
break;
case MATCH_MESSAGES_BY_CONTACT:
qb.setTables(MESSAGE_JOIN_CONTACT_TABLE);
qb.setProjectionMap(sMessagesProjectionMap);
appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getPathSegments().get(1));
appendWhere(whereClause, "contacts.username", "=", decodeURLSegment(url
.getPathSegments().get(2)));
final String sel = whereClause.toString();
final String q1 = qb.buildQuery(projectionIn, sel, null, null, null, null, null);
// Build the second query for frequent
qb = new SQLiteQueryBuilder();
qb.setTables(IN_MEMORY_MESSAGES_JOIN_CONTACT_TABLE);
qb.setProjectionMap(sInMemoryMessagesProjectionMap);
final String q2 = qb.buildQuery(projectionIn, sel, null, null, null, null, null);
// Put them together
final String q3 = qb.buildUnionQuery(new String[] { q1, q2 }, sort, null);
final SQLiteDatabase db2 = getDBHelper().getWritableDatabase();
Cursor c2 = db2.rawQueryWithFactory(null, q3, null, MESSAGE_JOIN_CONTACT_TABLE);
if ((c2 != null) && !isTemporary()) {
c2.setNotificationUri(getContext().getContentResolver(), url);
}
return c2;
case MATCH_INVITATIONS:
qb.setTables(TABLE_INVITATIONS);
break;
case MATCH_INVITATION:
qb.setTables(TABLE_INVITATIONS);
appendWhere(whereClause, Imps.Invitation._ID, "=", url.getPathSegments().get(1));
break;
case MATCH_GROUP_MEMBERS:
qb.setTables(TABLE_GROUP_MEMBERS);
break;
case MATCH_GROUP_MEMBERS_BY_GROUP:
qb.setTables(TABLE_GROUP_MEMBERS);
appendWhere(whereClause, Imps.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
break;
case MATCH_AVATARS:
qb.setTables(TABLE_AVATARS);
break;
case MATCH_AVATAR_BY_PROVIDER:
qb.setTables(TABLE_AVATARS);
appendWhere(whereClause, Imps.Avatars.ACCOUNT, "=", url.getPathSegments().get(2));
break;
case MATCH_CHATS:
qb.setTables(TABLE_CHATS);
break;
case MATCH_CHATS_ID:
qb.setTables(TABLE_CHATS);
appendWhere(whereClause, Imps.Chats.CONTACT_ID, "=", url.getPathSegments().get(1));
break;
case MATCH_CHATS_BY_ACCOUNT:
qb.setTables(TABLE_CHATS);
String accountStr = decodeURLSegment(url.getLastPathSegment());
appendWhere(
whereClause,
buildContactIdSelection(Imps.Chats.CONTACT_ID, Imps.Contacts.ACCOUNT + "='"
+ accountStr + "'"));
break;
case MATCH_PRESENCE:
qb.setTables(TABLE_PRESENCE);
break;
case MATCH_PRESENCE_ID:
qb.setTables(TABLE_PRESENCE);
appendWhere(whereClause, Imps.Presence.CONTACT_ID, "=", url.getPathSegments().get(1));
break;
case MATCH_SESSIONS:
qb.setTables(TABLE_SESSION_COOKIES);
break;
case MATCH_SESSIONS_BY_PROVIDER:
qb.setTables(TABLE_SESSION_COOKIES);
appendWhere(whereClause, Imps.SessionCookies.ACCOUNT, "=", url.getPathSegments().get(2));
break;
case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", url.getPathSegments().get(2));
// fall through
case MATCH_PROVIDER_SETTINGS_BY_ID:
appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", url.getPathSegments()
.get(1));
// fall through
case MATCH_PROVIDER_SETTINGS:
qb.setTables(TABLE_PROVIDER_SETTINGS);
break;
case MATCH_ACCOUNTS_STATUS:
qb.setTables(TABLE_ACCOUNT_STATUS);
break;
case MATCH_ACCOUNT_STATUS:
qb.setTables(TABLE_ACCOUNT_STATUS);
appendWhere(whereClause, Imps.AccountStatus.ACCOUNT, "=", url.getPathSegments().get(1));
break;
case MATCH_BRANDING_RESOURCE_MAP_CACHE:
qb.setTables(TABLE_BRANDING_RESOURCE_MAP_CACHE);
break;
// mcs and rmq queries
case MATCH_OUTGOING_RMQ_MESSAGES:
qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
break;
case MATCH_OUTGOING_HIGHEST_RMQ_ID:
qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
sort = "rmq_id DESC";
limit = "1";
break;
case MATCH_LAST_RMQ_ID:
qb.setTables(TABLE_LAST_RMQ_ID);
limit = "1";
break;
case MATCH_S2D_RMQ_IDS:
qb.setTables(TABLE_S2D_RMQ_IDS);
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
if (getDBHelper() == null)
return null;
// run the query
SQLiteDatabase db;
try {
db = getDBHelper().getReadableDatabase();
} catch (net.sqlcipher.database.SQLiteException e) {
// Failed to actually open - the passphrase must have been wrong - reset the helper
mDbHelper = null;
throw e;
}
net.sqlcipher.Cursor c = null;
if (!db.isOpen())
return null;
try {
qb.setDistinct(true);
c = qb.query(db, projectionIn, whereClause.toString(), selectionArgs, groupBy, null,
sort, limit);
if (c != null) {
switch (match) {
case MATCH_CHATTING_CONTACTS:
case MATCH_CONTACTS_BY_PROVIDER:
case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
case MATCH_CONTACTS_BAREBONE:
case MATCH_CONTACTS_JOIN_PRESENCE:
case MATCH_ONLINE_CONTACT_COUNT:
url = Imps.Contacts.CONTENT_URI;
break;
}
log("set notify url " + url);
c.setNotificationUri(getContext().getContentResolver(), url);
}
// c = new MyCrossProcessCursorWrapper(c);
return c;
} catch (Exception ex) {
LogCleaner.error(LOG_TAG, "query exc db caught ", ex);
return null;
}
catch (Error ex) {
LogCleaner.error(LOG_TAG, "query error db caught ", ex);
return null;
}
}
static class MyCrossProcessCursorWrapper extends net.sqlcipher.CrossProcessCursorWrapper {
public MyCrossProcessCursorWrapper(net.sqlcipher.Cursor cursor) {
super(cursor);
}
@Override
public void fillWindow(int position, CursorWindow window) {
if (position < 0 || position > getCount()) {
return;
}
window.acquireReference();
try {
moveToPosition(position - 1);
window.clear();
window.setStartPosition(position);
int columnNum = getColumnCount();
window.setNumColumns(columnNum);
boolean isFull = false;
int numRows = 10;
while (!isFull && --numRows > 0 && moveToNext() && window.allocRow()) {
for (int i = 0; i < columnNum; i++) {
String field = getString(i);
if (field != null) {
if (!window.putString(field, getPosition(), i)) {
window.freeLastRow();
isFull = true;
break;
}
} else {
if (!window.putNull(getPosition(), i)) {
window.freeLastRow();
isFull = true;
break;
}
}
}
}
} catch (IllegalStateException e) {
// simply ignore it
} finally {
window.releaseReference();
}
}
}
private void buildQueryContactsByProvider(SQLiteQueryBuilder qb, StringBuilder whereClause,
Uri url) {
qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
qb.setProjectionMap(sContactsProjectionMap);
// we don't really need the provider id in query. account id is enough.
appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getLastPathSegment());
}
@Override
public String getType(Uri url) {
int match = mUrlMatcher.match(url);
switch (match) {
case MATCH_PROVIDERS:
return Imps.Provider.CONTENT_TYPE;
case MATCH_PROVIDERS_BY_ID:
return Imps.Provider.CONTENT_ITEM_TYPE;
case MATCH_ACCOUNTS:
return Imps.Account.CONTENT_TYPE;
case MATCH_ACCOUNTS_BY_ID:
return Imps.Account.CONTENT_ITEM_TYPE;
case MATCH_CONTACTS:
case MATCH_CONTACTS_BY_PROVIDER:
case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
case MATCH_CONTACTS_BULK:
case MATCH_CONTACTS_BAREBONE:
case MATCH_CONTACTS_JOIN_PRESENCE:
return Imps.Contacts.CONTENT_TYPE;
case MATCH_CONTACT:
return Imps.Contacts.CONTENT_ITEM_TYPE;
case MATCH_CONTACTLISTS:
case MATCH_CONTACTLISTS_BY_PROVIDER:
return Imps.ContactList.CONTENT_TYPE;
case MATCH_CONTACTLIST:
return Imps.ContactList.CONTENT_ITEM_TYPE;
case MATCH_BLOCKEDLIST:
case MATCH_BLOCKEDLIST_BY_PROVIDER:
return Imps.BlockedList.CONTENT_TYPE;
case MATCH_CONTACTS_ETAGS:
case MATCH_CONTACTS_ETAG:
return Imps.ContactsEtag.CONTENT_TYPE;
case MATCH_MESSAGES:
case MATCH_MESSAGES_BY_CONTACT:
case MATCH_MESSAGES_BY_THREAD_ID:
case MATCH_MESSAGES_BY_PROVIDER:
case MATCH_MESSAGES_BY_ACCOUNT:
case MATCH_OTR_MESSAGES:
case MATCH_OTR_MESSAGES_BY_CONTACT:
case MATCH_OTR_MESSAGES_BY_THREAD_ID:
case MATCH_OTR_MESSAGES_BY_PROVIDER:
case MATCH_OTR_MESSAGES_BY_ACCOUNT:
return Imps.Messages.CONTENT_TYPE;
case MATCH_MESSAGE:
case MATCH_OTR_MESSAGE:
return Imps.Messages.CONTENT_ITEM_TYPE;
case MATCH_PRESENCE:
case MATCH_PRESENCE_BULK:
return Imps.Presence.CONTENT_TYPE;
case MATCH_AVATARS:
return Imps.Avatars.CONTENT_TYPE;
case MATCH_AVATAR:
return Imps.Avatars.CONTENT_ITEM_TYPE;
case MATCH_CHATS:
return Imps.Chats.CONTENT_TYPE;
case MATCH_CHATS_ID:
return Imps.Chats.CONTENT_ITEM_TYPE;
case MATCH_INVITATIONS:
return Imps.Invitation.CONTENT_TYPE;
case MATCH_INVITATION:
return Imps.Invitation.CONTENT_ITEM_TYPE;
case MATCH_GROUP_MEMBERS:
case MATCH_GROUP_MEMBERS_BY_GROUP:
return Imps.GroupMembers.CONTENT_TYPE;
case MATCH_SESSIONS:
case MATCH_SESSIONS_BY_PROVIDER:
return Imps.SessionCookies.CONTENT_TYPE;
case MATCH_PROVIDER_SETTINGS:
return Imps.ProviderSettings.CONTENT_TYPE;
case MATCH_ACCOUNTS_STATUS:
return Imps.AccountStatus.CONTENT_TYPE;
case MATCH_ACCOUNT_STATUS:
return Imps.AccountStatus.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URL");
}
}
// package scope for testing.
boolean insertBulkContacts(ContentValues values) {
// log("insertBulkContacts: begin");
ArrayList<String> usernames = getStringArrayList(values, Imps.Contacts.USERNAME);
ArrayList<String> nicknames = getStringArrayList(values, Imps.Contacts.NICKNAME);
int usernameCount = usernames.size();
int nicknameCount = nicknames.size();
if (usernameCount != nicknameCount) {
LogCleaner.warn(LOG_TAG, "[ImProvider] insertBulkContacts: input bundle "
+ "username & nickname lists have diff. length!");
return false;
}
ArrayList<String> contactTypeArray = getStringArrayList(values, Imps.Contacts.TYPE);
ArrayList<String> subscriptionStatusArray = getStringArrayList(values,
Imps.Contacts.SUBSCRIPTION_STATUS);
ArrayList<String> subscriptionTypeArray = getStringArrayList(values,
Imps.Contacts.SUBSCRIPTION_TYPE);
ArrayList<String> quickContactArray = getStringArrayList(values,
Imps.Contacts.QUICK_CONTACT);
ArrayList<String> rejectedArray = getStringArrayList(values, Imps.Contacts.REJECTED);
int sum = 0;
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
db.beginTransaction();
try {
Long provider = values.getAsLong(Imps.Contacts.PROVIDER);
Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
Long listId = values.getAsLong(Imps.Contacts.CONTACTLIST);
ContentValues contactValues = new ContentValues();
contactValues.put(Imps.Contacts.PROVIDER, provider);
contactValues.put(Imps.Contacts.ACCOUNT, account);
contactValues.put(Imps.Contacts.CONTACTLIST, listId);
ContentValues presenceValues = new ContentValues();
presenceValues.put(Imps.Presence.PRESENCE_STATUS, Imps.Presence.OFFLINE);
StringBuffer whereClause = new StringBuffer();
whereClause.append(Contacts.USERNAME);
whereClause.append("=?");
whereClause.append(" AND ");
whereClause.append(Contacts.ACCOUNT);
whereClause.append("=?");
for (int i = 0; i < usernameCount; i++) {
String username = usernames.get(i);
String nickname = nicknames.get(i);
int type = 0;
int subscriptionStatus = 0;
int subscriptionType = 0;
int quickContact = 0;
int rejected = 0;
try {
type = Integer.parseInt(contactTypeArray.get(i));
if (subscriptionStatusArray != null) {
subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
}
if (subscriptionTypeArray != null) {
subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
}
if (quickContactArray != null) {
quickContact = Integer.parseInt(quickContactArray.get(i));
}
if (rejectedArray != null) {
rejected = Integer.parseInt(rejectedArray.get(i));
}
} catch (NumberFormatException ex) {
LogCleaner.error(LOG_TAG, "insertBulkContacts: caught ", ex);
}
/*
log("insertBulkContacts[" + i + "] username=" +
username + ", nickname=" + nickname + ", type=" + type +
", subscriptionStatus=" + subscriptionStatus + ", subscriptionType=" +
subscriptionType + ", qc=" + quickContact);
*/
contactValues.put(Imps.Contacts.USERNAME, username);
contactValues.put(Imps.Contacts.NICKNAME, nickname);
contactValues.put(Imps.Contacts.TYPE, type);
if (subscriptionStatusArray != null) {
contactValues.put(Imps.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
}
if (subscriptionTypeArray != null) {
contactValues.put(Imps.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
}
if (quickContactArray != null) {
contactValues.put(Imps.Contacts.QUICK_CONTACT, quickContact);
}
if (rejectedArray != null) {
contactValues.put(Imps.Contacts.REJECTED, rejected);
}
long rowId;
/* save this code for when we add constraint (account, username) to the contacts
table
try {
rowId = db.insertOrThrow(TABLE_CONTACTS, USERNAME, contactValues);
} catch (android.database.sqlite.SQLiteConstraintException ex) {
log("insertBulkContacts: insert " + username + " caught " + ex);
// append username to the selection clause
updateSelection.delete(0, updateSelection.length());
updateSelection.append(Im.Contacts.USERNAME);
updateSelection.append("=?");
updateSelectionArgs[0] = username;
int updated = db.update(TABLE_CONTACTS, contactValues,
updateSelection.toString(), updateSelectionArgs);
if (DBG && updated != 1) {
log("insertBulkContacts: update " + username + " failed!");
}
}
*/
String[] columns = {"_id, username"};
String[] whereArgs = {username,account+""};
Cursor c = db.query(TABLE_CONTACTS, columns, whereClause.toString(), whereArgs, null,null,null,null);
boolean contactExists = (c != null && c.getCount() > 0);
if (c != null) c.close();
if (contactExists)
{
int rowsUpdated = db.update(TABLE_CONTACTS, contactValues, whereClause.toString(), whereArgs);
}
else
{
rowId = db.insert(TABLE_CONTACTS, USERNAME, contactValues);
if (rowId > 0) {
sum++;
// seed the presence for the new contact
log("### seedPresence for contact id " + rowId);
presenceValues.put(Imps.Presence.CONTACT_ID, rowId);
try {
db.insert(TABLE_PRESENCE, null, presenceValues);
} catch (android.database.sqlite.SQLiteConstraintException ex) {
LogCleaner.warn(LOG_TAG, "insertBulkContacts: seeding presence caught " + ex);
}
}
}
// yield the lock if anyone else is trying to
// perform a db operation here.
db.yieldIfContended();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// We know that we succeeded becuase endTransaction throws if the transaction failed.
log("insertBulkContacts: added " + sum + " contacts!");
return true;
}
private ArrayList<String> getStringArrayList(ContentValues values, String key) {
byte[] stored = values.getAsByteArray(key);
if (stored == null)
return null;
ByteArrayInputStream bis = new ByteArrayInputStream(stored);
try {
ObjectInputStream is = new ObjectInputStream(bis);
ArrayList<String> res = (ArrayList<String>) is.readObject();
is.close();
return res;
} catch (StreamCorruptedException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// package scope for testing.
int updateBulkContacts(ContentValues values, String userWhere) {
ArrayList<String> usernames = getStringArrayList(values, Imps.Contacts.USERNAME);
ArrayList<String> nicknames = getStringArrayList(values, Imps.Contacts.NICKNAME);
int usernameCount = usernames.size();
int nicknameCount = nicknames.size();
if (usernameCount != nicknameCount) {
LogCleaner.warn(LOG_TAG, "[ImProvider] updateBulkContacts: input bundle "
+ "username & nickname lists have diff. length!");
return 0;
}
ArrayList<String> contactTypeArray = getStringArrayList(values, Imps.Contacts.TYPE);
ArrayList<String> subscriptionStatusArray = getStringArrayList(values,
Imps.Contacts.SUBSCRIPTION_STATUS);
ArrayList<String> subscriptionTypeArray = getStringArrayList(values,
Imps.Contacts.SUBSCRIPTION_TYPE);
ArrayList<String> quickContactArray = getStringArrayList(values,
Imps.Contacts.QUICK_CONTACT);
ArrayList<String> rejectedArray = getStringArrayList(values, Imps.Contacts.REJECTED);
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
db.beginTransaction();
int sum = 0;
try {
Long provider = values.getAsLong(Imps.Contacts.PROVIDER);
Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
ContentValues contactValues = new ContentValues();
contactValues.put(Imps.Contacts.PROVIDER, provider);
contactValues.put(Imps.Contacts.ACCOUNT, account);
StringBuilder updateSelection = new StringBuilder();
String[] updateSelectionArgs = new String[1];
for (int i = 0; i < usernameCount; i++) {
String username = usernames.get(i);
String nickname = nicknames.get(i);
int type = 0;
int subscriptionStatus = 0;
int subscriptionType = 0;
int quickContact = 0;
int rejected = 0;
try {
type = Integer.parseInt(contactTypeArray.get(i));
subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
quickContact = Integer.parseInt(quickContactArray.get(i));
rejected = Integer.parseInt(rejectedArray.get(i));
} catch (NumberFormatException ex) {
LogCleaner.error(LOG_TAG, "insertBulkContacts: caught ",ex);
}
log("updateBulkContacts[" + i + "] username=" + username + ", nickname="
+ nickname + ", type=" + type + ", subscriptionStatus="
+ subscriptionStatus + ", subscriptionType=" + subscriptionType + ", qc="
+ quickContact);
contactValues.put(Imps.Contacts.USERNAME, username);
contactValues.put(Imps.Contacts.NICKNAME, nickname);
contactValues.put(Imps.Contacts.TYPE, type);
contactValues.put(Imps.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
contactValues.put(Imps.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
contactValues.put(Imps.Contacts.QUICK_CONTACT, quickContact);
contactValues.put(Imps.Contacts.REJECTED, rejected);
// append username to the selection clause
updateSelection.delete(0, updateSelection.length());
updateSelection.append(userWhere);
updateSelection.append(" AND ");
updateSelection.append(Imps.Contacts.USERNAME);
updateSelection.append("=?");
updateSelectionArgs[0] = username;
int numUpdated = db.update(TABLE_CONTACTS, contactValues,
updateSelection.toString(), updateSelectionArgs);
if (numUpdated == 0) {
LogCleaner.warn(LOG_TAG, "[ImProvider] updateBulkContacts: "
+ " update failed for selection = " + updateSelection);
} else {
sum += numUpdated;
}
// yield the lock if anyone else is trying to
// perform a db operation here.
db.yieldIfContended();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
log("updateBulkContacts: " + sum + " entries updated");
return sum;
}
/**
* make sure the presence for all contacts of a given account is set to
* offline, and each contact has a presence row associated with it. However,
* this method does not remove presences for which the corresponding
* contacts no longer exist. That's probably ok since presence is kept in
* memory, so it won't stay around for too long. Here is the algorithm.
*
* 1. for all presence that have a corresponding contact, make it OFFLINE.
* This is one sqlite call. 2. query for all the contacts that don't have a
* presence, and add a presence row for them.
*
* TODO simplify the presence management! The desire is to have a presence
* row for each TODO contact in the database, so later we can just call
* update() on the presence rows TODO instead of checking for the existence
* of presence first. The assumption is we get TODO presence updates much
* more frequently. However, the logic to maintain that goal is TODO overly
* complicated. One possible solution is to use insert_or_replace the
* presence rows TODO when updating the presence. That way we don't always
* need to maintain an empty presence TODO row for each contact.
*
* @param account the account of the contacts for which we want to create
* seed presence rows.
*/
private void seedInitialPresenceByAccount(long account) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_CONTACTS);
qb.setProjectionMap(sContactsProjectionMap);
mQueryContactIdSelectionArgs1[0] = String.valueOf(account);
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
db.beginTransaction();
Cursor c = null;
try {
ContentValues presenceValues = new ContentValues();
presenceValues.put(Imps.Presence.PRESENCE_STATUS, Imps.Presence.OFFLINE);
presenceValues.put(Imps.Presence.PRESENCE_CUSTOM_STATUS, "");
// update all the presence for the account so they are offline
StringBuilder buf = new StringBuilder();
buf.append(Imps.Presence.CONTACT_ID);
buf.append(" in (select ");
buf.append(Imps.Contacts._ID);
buf.append(" from ");
buf.append(TABLE_CONTACTS);
buf.append(" where ");
buf.append(Imps.Contacts.ACCOUNT);
buf.append("=?) ");
String selection = buf.toString();
log("seedInitialPresence: reset presence selection=" + selection);
int count = db.update(TABLE_PRESENCE, presenceValues, selection,
mQueryContactIdSelectionArgs1);
log("seedInitialPresence: reset " + count + " presence rows to OFFLINE");
// for in-memory presence table, add a presence row for each contact that
// doesn't have a presence. in-memory presence table isn't reliable, and goes away
// when device reboot or IMProvider process dies, so we can't rely on each contact
// have a corresponding presence.
{
log("seedInitialPresence: contacts_with_no_presence_selection => "
+ CONTACTS_WITH_NO_PRESENCE_SELECTION);
}
c = qb.query(db, CONTACT_ID_PROJECTION, CONTACTS_WITH_NO_PRESENCE_SELECTION,
mQueryContactIdSelectionArgs1, null, null, null, null);
log("seedInitialPresence: found " + c.getCount() + " contacts w/o presence");
count = 0;
while (c.moveToNext()) {
long id = c.getLong(CONTACT_ID_COLUMN);
presenceValues.put(Imps.Presence.CONTACT_ID, id);
try {
if (db.insert(TABLE_PRESENCE, null, presenceValues) > 0) {
count++;
}
} catch (SQLiteConstraintException ex) {
// we could possibly catch this exception, since there could be a presence
// row with the same contact_id. That's fine, just ignore the error
log("seedInitialPresence: insert presence for contact_id " + id
+ " failed, caught " + ex);
}
}
log("seedInitialPresence: added " + count + " new presence rows");
db.setTransactionSuccessful();
} finally {
if (c != null) {
c.close();
}
db.endTransaction();
}
}
private int updateBulkPresence(ContentValues values, SQLiteDatabase db, String userWhere, String[] whereArgs) {
ArrayList<String> usernames = getStringArrayList(values, Imps.Contacts.USERNAME);
int count = usernames.size();
Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
ArrayList<String> priorityArray = getStringArrayList(values, Imps.Presence.PRIORITY);
ArrayList<String> modeArray = getStringArrayList(values, Imps.Presence.PRESENCE_STATUS);
ArrayList<String> statusArray = getStringArrayList(values,
Imps.Presence.PRESENCE_CUSTOM_STATUS);
ArrayList<String> clientTypeArray = getStringArrayList(values, Imps.Presence.CLIENT_TYPE);
ArrayList<String> resourceArray = getStringArrayList(values, Imps.Presence.JID_RESOURCE);
// append username to the selection clause
StringBuilder buf = new StringBuilder();
if (!TextUtils.isEmpty(userWhere)) {
buf.append(userWhere);
buf.append(" AND ");
}
buf.append(Imps.Presence.CONTACT_ID);
buf.append(" in (select ");
buf.append(Imps.Contacts._ID);
buf.append(" from ");
buf.append(TABLE_CONTACTS);
buf.append(" where ");
buf.append(Imps.Contacts.ACCOUNT);
buf.append("=? AND ");
// use username LIKE ? for case insensitive comparison
buf.append(Imps.Contacts.USERNAME);
buf.append(" LIKE ?)");
/*
AND (");
buf.append(Imps.Presence.PRIORITY);
buf.append("<=? OR ");
buf.append(Imps.Presence.PRIORITY);
buf.append(" IS NULL OR ");
buf.append(Imps.Presence.JID_RESOURCE);
buf.append("=?)");
*/
String selection = buf.toString();
log("updateBulkPresence: selection => " + selection);
int numArgs = (whereArgs != null ? whereArgs.length + 2 : 2);
String[] selectionArgs = new String[numArgs];
int selArgsIndex = 0;
if (whereArgs != null) {
for (selArgsIndex = 0; selArgsIndex < numArgs - 1; selArgsIndex++) {
selectionArgs[selArgsIndex] = whereArgs[selArgsIndex];
}
}
db.beginTransaction();
int sum = 0;
try {
ContentValues presenceValues = new ContentValues();
for (int i = 0; i < count; i++) {
String username = usernames.get(i);
int priority = 0;
int mode = 0;
String status = statusArray.get(i);
String jidResource = resourceArray == null ? "" : resourceArray.get(i);
int clientType = Imps.Presence.CLIENT_TYPE_DEFAULT;
try {
if (priorityArray != null) {
priority = Integer.parseInt(priorityArray.get(i));
}
if (modeArray != null) {
mode = Integer.parseInt(modeArray.get(i));
}
if (clientTypeArray != null) {
clientType = Integer.parseInt(clientTypeArray.get(i));
}
} catch (NumberFormatException ex) {
LogCleaner.error(LOG_TAG, "[ImProvider] updateBulkPresence: caught",ex);
}
log("updateBulkPresence[" + i + "] account=" + account + " username=" + username + ", priority="
+ priority + ", mode=" + mode + ", status=" + status + ", resource="
+ jidResource + ", clientType=" + clientType);
presenceValues.put(Imps.Presence.PRESENCE_STATUS, mode);
presenceValues.put(Imps.Presence.PRIORITY, priority);
presenceValues.put(Imps.Presence.PRESENCE_CUSTOM_STATUS, status);
presenceValues.put(Imps.Presence.CLIENT_TYPE, clientType);
presenceValues.put(Imps.Presence.JID_RESOURCE, jidResource);
// fill in the selection args
int idx = selArgsIndex;
selectionArgs[idx++] = String.valueOf(account);
selectionArgs[idx++] = username;
//selectionArgs[idx++] = String.valueOf(priority);
//selectionArgs[idx] = jidResource;
int numUpdated = db
.update(TABLE_PRESENCE, presenceValues, selection, selectionArgs);
if (numUpdated == 0)
{
LogCleaner.debug(LOG_TAG, "[ImProvider] updateBulkPresence: " + username + " updated " + numUpdated);
}
else
{
sum += numUpdated;
}
// yield the lock if anyone else is trying to
// perform a db operation here.
db.yieldIfContended();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
log("updateBulkPresence: " + sum + " entries updated");
return sum;
}
private Uri insertInternal(Uri url, ContentValues initialValues) {
Uri resultUri = null;
long rowID = 0;
long account = 0;
String contact = null;
long threadId = 0;
boolean notifyContactListContentUri = false;
boolean notifyContactContentUri = false;
boolean notifyMessagesContentUri = false;
boolean notifyMessagesByContactContentUri = false;
boolean notifyMessagesByThreadIdContentUri = false;
boolean notifyProviderAccountContentUri = false;
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
int match = mUrlMatcher.match(url);
log("insert to " + url + ", match " + match);
switch (match) {
case MATCH_PROVIDERS:
// Insert into the providers table
rowID = db.insert(TABLE_PROVIDERS, "name", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Provider.CONTENT_URI + "/" + rowID);
}
notifyProviderAccountContentUri = true;
break;
case MATCH_ACCOUNTS:
// Insert into the accounts table
rowID = db.insert(TABLE_ACCOUNTS, "name", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Account.CONTENT_URI + "/" + rowID);
}
notifyProviderAccountContentUri = true;
break;
case MATCH_CONTACTS_BY_PROVIDER:
appendValuesFromUrl(initialValues, url, Imps.Contacts.PROVIDER, Imps.Contacts.ACCOUNT);
// fall through
case MATCH_CONTACTS:
case MATCH_CONTACTS_BAREBONE:
// Insert into the contacts table
rowID = db.insert(TABLE_CONTACTS, USERNAME, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Contacts.CONTENT_URI + "/" + rowID);
}
notifyContactContentUri = true;
break;
case MATCH_CONTACTS_BULK:
if (insertBulkContacts(initialValues)) {
// notify change using the "content://im/contacts" url,
// so the change will be observed by listeners interested
// in contacts changes.
resultUri = Imps.Contacts.CONTENT_URI;
}
notifyContactContentUri = true;
break;
case MATCH_CONTACTLISTS_BY_PROVIDER:
appendValuesFromUrl(initialValues, url, Imps.ContactList.PROVIDER,
Imps.ContactList.ACCOUNT);
// fall through
case MATCH_CONTACTLISTS:
// Insert into the contactList table
rowID = db.insert(TABLE_CONTACT_LIST, "name", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.ContactList.CONTENT_URI + "/" + rowID);
}
notifyContactListContentUri = true;
break;
case MATCH_BLOCKEDLIST_BY_PROVIDER:
appendValuesFromUrl(initialValues, url, Imps.BlockedList.PROVIDER,
Imps.BlockedList.ACCOUNT);
// fall through
case MATCH_BLOCKEDLIST:
// Insert into the blockedList table
rowID = db.insert(TABLE_BLOCKED_LIST, "username", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.BlockedList.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_CONTACTS_ETAGS:
rowID = db.replace(TABLE_CONTACTS_ETAG, Imps.ContactsEtag.ETAG, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.ContactsEtag.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_MESSAGES_BY_CONTACT:
String accountStr = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
initialValues.put(Imps.Messages.THREAD_ID, getContactId(db, accountStr, contact));
notifyMessagesContentUri = true;
// Insert into the messages table.
rowID = db.insert(TABLE_MESSAGES, "thread_id", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Messages.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_MESSAGES_BY_THREAD_ID:
appendValuesFromUrl(initialValues, url, Imps.Messages.THREAD_ID);
// fall through
case MATCH_MESSAGES:
// Insert into the messages table.
notifyMessagesContentUri = true;
rowID = db.insert(TABLE_MESSAGES, "thread_id", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Messages.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_OTR_MESSAGES_BY_CONTACT:
String accountStr2 = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr2);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
initialValues.put(Imps.Messages.THREAD_ID, getContactId(db, accountStr2, contact));
notifyMessagesByContactContentUri = true;
// Insert into the in-memory messages table.
rowID = db.insert(TABLE_IN_MEMORY_MESSAGES, "thread_id", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Messages.OTR_MESSAGES_CONTENT_URI + "/" + rowID);
}
break;
case MATCH_OTR_MESSAGES_BY_THREAD_ID:
try {
threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
initialValues.put(Imps.Messages.THREAD_ID, threadId);
notifyMessagesByThreadIdContentUri = true;
// fall through
case MATCH_OTR_MESSAGES:
// Insert into the messages table.
rowID = db.insert(TABLE_IN_MEMORY_MESSAGES, "thread_id", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Messages.OTR_MESSAGES_CONTENT_URI + "/" + rowID);
}
break;
case MATCH_INVITATIONS:
rowID = db.insert(TABLE_INVITATIONS, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Invitation.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_GROUP_MEMBERS:
rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.GroupMembers.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_GROUP_MEMBERS_BY_GROUP:
appendValuesFromUrl(initialValues, url, Imps.GroupMembers.GROUP);
rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.GroupMembers.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_AVATAR_BY_PROVIDER:
appendValuesFromUrl(initialValues, url, Imps.Avatars.PROVIDER, Imps.Avatars.ACCOUNT);
// fall through
case MATCH_AVATARS:
// Insert into the avatars table
rowID = db.replace(TABLE_AVATARS, "contact", initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Avatars.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_CHATS_ID:
appendValuesFromUrl(initialValues, url, Imps.Chats.CONTACT_ID);
// fall through
case MATCH_CHATS:
// Insert into the chats table
initialValues.put(Imps.Chats.SHORTCUT, -1);
rowID = db.replace(TABLE_CHATS, Imps.Chats.CONTACT_ID, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Chats.CONTENT_URI + "/" + rowID);
addToQuickSwitch(rowID);
}
notifyContactContentUri = true;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_PRESENCE:
rowID = db.replace(TABLE_PRESENCE, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.Presence.CONTENT_URI + "/" + rowID);
}
notifyContactContentUri = true;
break;
case MATCH_PRESENCE_SEED_BY_ACCOUNT:
try {
seedInitialPresenceByAccount(Long.parseLong(url.getLastPathSegment()));
resultUri = Imps.Presence.CONTENT_URI;
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
break;
case MATCH_SESSIONS_BY_PROVIDER:
appendValuesFromUrl(initialValues, url, Imps.SessionCookies.PROVIDER,
Imps.SessionCookies.ACCOUNT);
// fall through
case MATCH_SESSIONS:
rowID = db.insert(TABLE_SESSION_COOKIES, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.SessionCookies.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_PROVIDER_SETTINGS:
rowID = db.replace(TABLE_PROVIDER_SETTINGS, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.ProviderSettings.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_ACCOUNTS_STATUS:
rowID = db.replace(TABLE_ACCOUNT_STATUS, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.AccountStatus.CONTENT_URI + "/" + rowID);
}
notifyProviderAccountContentUri = true;
break;
case MATCH_BRANDING_RESOURCE_MAP_CACHE:
rowID = db.insert(TABLE_BRANDING_RESOURCE_MAP_CACHE, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.BrandingResourceMapCache.CONTENT_URI + "/" + rowID);
}
break;
// mcs/rmq stuff
case MATCH_OUTGOING_RMQ_MESSAGES:
rowID = db.insert(TABLE_OUTGOING_RMQ_MESSAGES, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.OutgoingRmq.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_LAST_RMQ_ID:
rowID = db.replace(TABLE_LAST_RMQ_ID, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.LastRmqId.CONTENT_URI + "/" + rowID);
}
break;
case MATCH_S2D_RMQ_IDS:
rowID = db.insert(TABLE_S2D_RMQ_IDS, null, initialValues);
if (rowID > 0) {
resultUri = Uri.parse(Imps.ServerToDeviceRmqIds.CONTENT_URI + "/" + rowID);
}
break;
default:
throw new UnsupportedOperationException("Cannot insert into URL: " + url);
}
// TODO: notify the data change observer?
if (resultUri != null) {
ContentResolver resolver = getContext().getContentResolver();
// In most case, we query contacts with presence and chats joined, thus
// we should also notify that contacts changes when presence or chats changed.
if (notifyContactContentUri) {
resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
}
if (notifyContactListContentUri) {
resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
}
if (notifyMessagesContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
}
if (notifyMessagesByContactContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
}
if (notifyMessagesByThreadIdContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
}
if (notifyProviderAccountContentUri) {
log("notify insert for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
}
}
return resultUri;
}
private void appendValuesFromUrl(ContentValues values, Uri url, String... columns) {
if (url.getPathSegments().size() <= columns.length) {
throw new IllegalArgumentException("Not enough values in url");
}
for (int i = 0; i < columns.length; i++) {
if (values.containsKey(columns[i])) {
throw new UnsupportedOperationException("Cannot override the value for "
+ columns[i]);
}
values.put(columns[i], decodeURLSegment(url.getPathSegments().get(i + 1)));
}
}
private long getContactId(final SQLiteDatabase db, final String accountId, final String contact) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_CONTACTS);
qb.setProjectionMap(sContactsProjectionMap);
mQueryContactIdSelectionArgs2[0] = accountId;
mQueryContactIdSelectionArgs2[1] = contact;
Cursor c = qb.query(db, CONTACT_ID_PROJECTION, CONTACT_ID_QUERY_SELECTION,
mQueryContactIdSelectionArgs2, null, null, null, null);
long contactId = 0;
try {
if (c.moveToFirst()) {
contactId = c.getLong(CONTACT_ID_COLUMN);
}
} finally {
c.close();
}
return contactId;
}
// Quick-switch management
// The chat UI provides slots (0, 9, .., 1) for the first 10 chats. This allows you to
// quickly switch between these chats by chording menu+#. We number from the right end of
// the number row and move leftward to make an easier two-hand chord with the menu button
// on the left side of the keyboard.
private void addToQuickSwitch(long newRow) {
// Since there are fewer than 10, there must be an empty slot. Let's find it.
int slot = findEmptyQuickSwitchSlot();
if (slot == -1) {
return;
}
updateSlotForChat(newRow, slot);
}
// If there are more than 10 chats and one with a quick switch slot ends then pick a chat
// that doesn't have a slot and have it inhabit the newly emptied slot.
private void backfillQuickSwitchSlots() {
// Find all the chats without a quick switch slot, and order
Cursor c = query(Imps.Chats.CONTENT_URI, BACKFILL_PROJECTION, Imps.Chats.SHORTCUT + "=-1",
null, Imps.Chats.LAST_MESSAGE_DATE + " DESC");
try {
if (c.getCount() < 1) {
return;
}
int slot = findEmptyQuickSwitchSlot();
if (slot != -1) {
c.moveToFirst();
long id = c.getLong(c.getColumnIndex(Imps.Chats._ID));
updateSlotForChat(id, slot);
}
} finally {
c.close();
}
}
private int updateSlotForChat(long chatId, int slot) {
ContentValues values = new ContentValues();
values.put(Imps.Chats.SHORTCUT, slot);
return update(Imps.Chats.CONTENT_URI, values, Imps.Chats._ID + "=?",
new String[] { Long.toString(chatId) });
}
private int findEmptyQuickSwitchSlot() {
Cursor c = queryInternal(Imps.Chats.CONTENT_URI, FIND_SHORTCUT_PROJECTION, null, null, null);
final int N = c.getCount();
try {
// If there are 10 or more chats then all the quick switch slots are already filled
if (N >= 10) {
return -1;
}
int slots = 0;
int column = c.getColumnIndex(Imps.Chats.SHORTCUT);
// The map is here because numbers go from 0-9, but we want to assign slots in
// 0, 9, 8, ..., 1 order to match the right-to-left reading of the number row
// on the keyboard.
int[] map = new int[] { 0, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
// Mark all the slots that are in use
// The shortcuts represent actual keyboard number row keys, and not ordinals.
// So 7 would mean the shortcut is the 7 key on the keyboard and NOT the 7th
// shortcut. The passing of slot through map[] below maps these keyboard key
// shortcuts into an ordinal bit position in the 'slots' bitfield.
for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
int slot = c.getInt(column);
if (slot != -1) {
slots |= (1 << map[slot]);
}
}
// Try to find an empty one
// As we exit this, the push of i through map[] maps the ordinal bit position
// in the 'slots' bitfield onto a key on the number row of the device keyboard.
// The keyboard key is what is used to designate the shortcut.
for (int i = 0; i < 10; i++) {
if ((slots & (1 << i)) == 0) {
return map[i];
}
}
return -1;
} finally {
c.close();
}
}
/** manual trigger for deleting contacts */
private static final String DELETE_PRESENCE_SELECTION = Imps.Presence.CONTACT_ID
+ " in (select " + PRESENCE_CONTACT_ID
+ " from " + TABLE_PRESENCE
+ " left outer join " + TABLE_CONTACTS
+ " on " + PRESENCE_CONTACT_ID + '='
+ CONTACT_ID + " where " + CONTACT_ID
+ " IS NULL)";
private static final String CHATS_CONTACT_ID = TABLE_CHATS + '.' + Imps.Chats.CONTACT_ID;
private static final String DELETE_CHATS_SELECTION = Imps.Chats.CONTACT_ID + " in (select "
+ CHATS_CONTACT_ID + " from "
+ TABLE_CHATS + " left outer join "
+ TABLE_CONTACTS + " on "
+ CHATS_CONTACT_ID + '=' + CONTACT_ID
+ " where " + CONTACT_ID + " IS NULL)";
private static final String GROUP_MEMBER_ID = TABLE_GROUP_MEMBERS + '.'
+ Imps.GroupMembers.GROUP;
private static final String DELETE_GROUP_MEMBER_SELECTION = Imps.GroupMembers.GROUP
+ " in (select " + GROUP_MEMBER_ID
+ " from " + TABLE_GROUP_MEMBERS
+ " left outer join "
+ TABLE_CONTACTS + " on "
+ GROUP_MEMBER_ID + '='
+ CONTACT_ID + " where "
+ CONTACT_ID + " IS NULL)";
private static final String GROUP_MESSAGES_ID = TABLE_MESSAGES + '.' + Imps.Messages.THREAD_ID;
private static final String DELETE_GROUP_MESSAGES_SELECTION = Imps.Messages.THREAD_ID
+ " in (select "
+ GROUP_MESSAGES_ID + " from "
+ TABLE_MESSAGES
+ " left outer join "
+ TABLE_CONTACTS + " on "
+ GROUP_MESSAGES_ID + '='
+ CONTACT_ID + " where "
+ CONTACT_ID + " IS NULL)";
private void performContactRemovalCleanup(long contactId) {
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
if (contactId > 0) {
StringBuilder buf = new StringBuilder();
// delete presence
buf.append(Imps.Presence.CONTACT_ID).append('=').append(contactId);
deleteWithSelection(db, TABLE_PRESENCE, buf.toString(), null);
// delete group memebers
buf.delete(0, buf.length());
buf.append(Imps.GroupMembers.GROUP).append('=').append(contactId);
deleteWithSelection(db, TABLE_GROUP_MEMBERS, buf.toString(), null);
} else {
// delete presence
deleteWithSelection(db, TABLE_PRESENCE, DELETE_PRESENCE_SELECTION, null);
// delete group members
deleteWithSelection(db, TABLE_GROUP_MEMBERS, DELETE_GROUP_MEMBER_SELECTION, null);
}
}
private void deleteWithSelection(SQLiteDatabase db, String tableName, String selection,
String[] selectionArgs) {
log("deleteWithSelection: table " + tableName + ", selection => " + selection);
int count = db.delete(tableName, selection, selectionArgs);
log("deleteWithSelection: deleted " + count + " rows");
}
private String buildContactIdSelection(String columnName, String contactSelection) {
StringBuilder buf = new StringBuilder();
buf.append(columnName);
buf.append(" in (select ");
buf.append(Imps.Contacts._ID);
buf.append(" from ");
buf.append(TABLE_CONTACTS);
buf.append(" where ");
buf.append(contactSelection);
buf.append(")");
return buf.toString();
}
private int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
String tableToChange;
// In some cases a given url requires that we delete rows from more than one
// table. The motivating example is deleting messages from both the on disk
// and in memory messages tables.
String tableToChange2 = null;
String idColumnName = null;
String changedItemId = null;
String provider = null;
String accountStr = null;
long account = 0;
String contact = null;
long threadId = 0;
StringBuilder whereClause = new StringBuilder();
if (userWhere != null) {
whereClause.append(userWhere);
}
boolean notifyMessagesContentUri = false;
boolean notifyMessagesByContactContentUri = false;
boolean notifyMessagesByThreadIdContentUri = false;
boolean notifyContactListContentUri = false;
boolean notifyProviderAccountContentUri = false;
int match = mUrlMatcher.match(url);
boolean contactDeleted = false;
long deletedContactId = 0;
boolean backfillQuickSwitchSlots = false;
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
switch (match) {
case MATCH_PROVIDERS_BY_ID:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_PROVIDERS:
tableToChange = TABLE_PROVIDERS;
notifyProviderAccountContentUri = true;
break;
case MATCH_ACCOUNTS_BY_ID:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_ACCOUNTS:
tableToChange = TABLE_ACCOUNTS;
notifyProviderAccountContentUri = true;
break;
case MATCH_ACCOUNT_STATUS:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_ACCOUNTS_STATUS:
tableToChange = TABLE_ACCOUNT_STATUS;
notifyProviderAccountContentUri = true;
break;
case MATCH_CONTACTS:
case MATCH_CONTACTS_BAREBONE:
tableToChange = TABLE_CONTACTS;
contactDeleted = true;
break;
case MATCH_CONTACT:
tableToChange = TABLE_CONTACTS;
changedItemId = url.getPathSegments().get(1);
try {
deletedContactId = Long.parseLong(changedItemId);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contactDeleted = true;
break;
case MATCH_CONTACTS_BY_PROVIDER:
tableToChange = TABLE_CONTACTS;
appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getPathSegments().get(2));
contactDeleted = true;
break;
case MATCH_CONTACTLISTS_BY_PROVIDER:
appendWhere(whereClause, Imps.ContactList.ACCOUNT, "=", url.getPathSegments().get(2));
// fall through
case MATCH_CONTACTLISTS:
tableToChange = TABLE_CONTACT_LIST;
notifyContactListContentUri = true;
break;
case MATCH_CONTACTLIST:
tableToChange = TABLE_CONTACT_LIST;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_BLOCKEDLIST:
tableToChange = TABLE_BLOCKED_LIST;
break;
case MATCH_BLOCKEDLIST_BY_PROVIDER:
tableToChange = TABLE_BLOCKED_LIST;
appendWhere(whereClause, Imps.BlockedList.ACCOUNT, "=", url.getPathSegments().get(2));
break;
case MATCH_CONTACTS_ETAGS:
tableToChange = TABLE_CONTACTS_ETAG;
break;
case MATCH_CONTACTS_ETAG:
tableToChange = TABLE_CONTACTS_ETAG;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_MESSAGES:
tableToChange = TABLE_MESSAGES;
break;
case MATCH_MESSAGES_BY_CONTACT:
tableToChange = TABLE_MESSAGES;
tableToChange2 = TABLE_IN_MEMORY_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
getContactId(db, accountStr, contact));
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGES_BY_THREAD_ID:
tableToChange = TABLE_MESSAGES;
tableToChange2 = TABLE_IN_MEMORY_MESSAGES;
try {
threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGES_BY_PROVIDER:
tableToChange = TABLE_MESSAGES;
provider = decodeURLSegment(url.getPathSegments().get(1));
appendWhere(
whereClause,
buildContactIdSelection(Imps.Messages.THREAD_ID, Imps.Contacts.PROVIDER + "='"
+ provider + "'"));
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGES_BY_ACCOUNT:
tableToChange = TABLE_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
appendWhere(
whereClause,
buildContactIdSelection(Imps.Messages.THREAD_ID, Imps.Contacts.ACCOUNT + "='"
+ accountStr + "'"));
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGE:
tableToChange = TABLE_MESSAGES;
changedItemId = url.getPathSegments().get(1);
notifyMessagesContentUri = true;
break;
case MATCH_OTR_MESSAGES:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
break;
case MATCH_OTR_MESSAGES_BY_CONTACT:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
getContactId(db, accountStr, contact));
notifyMessagesByContactContentUri = true;
break;
case MATCH_OTR_MESSAGES_BY_THREAD_ID:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
try {
threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
notifyMessagesByThreadIdContentUri = true;
break;
case MATCH_OTR_MESSAGES_BY_PROVIDER:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
provider = decodeURLSegment(url.getPathSegments().get(1));
appendWhere(
whereClause,
buildContactIdSelection(Imps.Messages.THREAD_ID, Imps.Contacts.PROVIDER + "='"
+ provider + "'"));
log("delete (MATCH_OTR_MESSAGES_BY_PROVIDER) sel => " + whereClause);
notifyMessagesContentUri = true;
break;
case MATCH_OTR_MESSAGES_BY_ACCOUNT:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
appendWhere(
whereClause,
buildContactIdSelection(Imps.Messages.THREAD_ID, Imps.Contacts.ACCOUNT + "='"
+ accountStr + "'"));
log("delete (MATCH_OTR_MESSAGES_BY_ACCOUNT) sel => " + whereClause);
notifyMessagesContentUri = true;
break;
case MATCH_OTR_MESSAGE:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
changedItemId = url.getPathSegments().get(1);
notifyMessagesContentUri = true;
break;
case MATCH_GROUP_MEMBERS:
tableToChange = TABLE_GROUP_MEMBERS;
break;
case MATCH_GROUP_MEMBERS_BY_GROUP:
tableToChange = TABLE_GROUP_MEMBERS;
appendWhere(whereClause, Imps.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
break;
case MATCH_INVITATIONS:
tableToChange = TABLE_INVITATIONS;
break;
case MATCH_INVITATION:
tableToChange = TABLE_INVITATIONS;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_AVATARS:
tableToChange = TABLE_AVATARS;
break;
case MATCH_AVATAR:
tableToChange = TABLE_AVATARS;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_AVATAR_BY_PROVIDER:
tableToChange = TABLE_AVATARS;
changedItemId = url.getPathSegments().get(2);
idColumnName = Imps.Avatars.ACCOUNT;
break;
case MATCH_CHATS:
tableToChange = TABLE_CHATS;
backfillQuickSwitchSlots = true;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_CHATS_BY_ACCOUNT:
tableToChange = TABLE_CHATS;
accountStr = decodeURLSegment(url.getLastPathSegment());
appendWhere(
whereClause,
buildContactIdSelection(Imps.Chats.CONTACT_ID, Imps.Contacts.ACCOUNT + "='"
+ accountStr + "'"));
log("delete (MATCH_CHATS_BY_ACCOUNT) sel => " + whereClause);
changedItemId = null;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_CHATS_ID:
tableToChange = TABLE_CHATS;
changedItemId = url.getPathSegments().get(1);
idColumnName = Imps.Chats.CONTACT_ID;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_PRESENCE:
tableToChange = TABLE_PRESENCE;
break;
case MATCH_PRESENCE_ID:
tableToChange = TABLE_PRESENCE;
changedItemId = url.getPathSegments().get(1);
idColumnName = Imps.Presence.CONTACT_ID;
break;
case MATCH_PRESENCE_BY_ACCOUNT:
tableToChange = TABLE_PRESENCE;
accountStr = decodeURLSegment(url.getLastPathSegment());
appendWhere(
whereClause,
buildContactIdSelection(Imps.Presence.CONTACT_ID, Imps.Contacts.ACCOUNT + "='"
+ accountStr + "'"));
log("delete (MATCH_PRESENCE_BY_ACCOUNT): sel => " + whereClause);
changedItemId = null;
break;
case MATCH_SESSIONS:
tableToChange = TABLE_SESSION_COOKIES;
break;
case MATCH_SESSIONS_BY_PROVIDER:
tableToChange = TABLE_SESSION_COOKIES;
changedItemId = url.getPathSegments().get(2);
idColumnName = Imps.SessionCookies.ACCOUNT;
break;
case MATCH_PROVIDER_SETTINGS_BY_ID:
tableToChange = TABLE_PROVIDER_SETTINGS;
changedItemId = url.getPathSegments().get(1);
idColumnName = Imps.ProviderSettings.PROVIDER;
break;
case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
tableToChange = TABLE_PROVIDER_SETTINGS;
String providerId = url.getPathSegments().get(1);
String name = url.getPathSegments().get(2);
appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", providerId);
appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", name);
break;
case MATCH_BRANDING_RESOURCE_MAP_CACHE:
tableToChange = TABLE_BRANDING_RESOURCE_MAP_CACHE;
break;
// mcs/rmq stuff
case MATCH_OUTGOING_RMQ_MESSAGES:
tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
break;
case MATCH_LAST_RMQ_ID:
tableToChange = TABLE_LAST_RMQ_ID;
break;
case MATCH_S2D_RMQ_IDS:
tableToChange = TABLE_S2D_RMQ_IDS;
break;
default:
throw new UnsupportedOperationException("Cannot delete that URL: " + url);
}
if (idColumnName == null) {
idColumnName = "_id";
}
if (changedItemId != null) {
appendWhere(whereClause, idColumnName, "=", changedItemId);
}
log("delete from " + url + " WHERE " + whereClause);
int count = db.delete(tableToChange, whereClause.toString(), whereArgs);
// see the comment at the declaration of tableToChange2 for an explanation
if (tableToChange2 != null) {
count += db.delete(tableToChange2, whereClause.toString(), whereArgs);
}
if (contactDeleted && count > 0) {
// since the contact cleanup triggers no longer work for cross database tables,
// we have to do it by hand here.
performContactRemovalCleanup(deletedContactId);
}
if (count > 0) {
ContentResolver resolver = getContext().getContentResolver();
// In most case, we query contacts with presence and chats joined, thus
// we should also notify that contacts changes when presence or chats changed.
if (match == MATCH_CHATS || match == MATCH_CHATS_ID || match == MATCH_PRESENCE
|| match == MATCH_PRESENCE_ID || match == MATCH_CONTACTS_BAREBONE) {
resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
}
if (notifyMessagesContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
}
if (notifyMessagesByContactContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
}
if (notifyMessagesByThreadIdContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
}
if (notifyContactListContentUri) {
resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
}
if (notifyProviderAccountContentUri) {
log("notify delete for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
}
if (backfillQuickSwitchSlots) {
backfillQuickSwitchSlots();
}
}
return count;
}
private int updateInternal(Uri url, ContentValues values, String userWhere, String[] whereArgs) {
String tableToChange;
String idColumnName = null;
String changedItemId = null;
String accountStr = null;
long account = 0;
String contact = null;
String packetId = null;
long threadId = 0;
int count = 0;
StringBuilder whereClause = new StringBuilder();
if (userWhere != null) {
whereClause.append(userWhere);
}
boolean notifyMessagesContentUri = false;
boolean notifyMessagesByContactContentUri = false;
boolean notifyMessagesByThreadIdContentUri = false;
boolean notifyContactListContentUri = false;
boolean notifyProviderAccountContentUri = false;
int match = mUrlMatcher.match(url);
final SQLiteDatabase db = getDBHelper().getWritableDatabase();
switch (match) {
case MATCH_PROVIDERS_BY_ID:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_PROVIDERS:
tableToChange = TABLE_PROVIDERS;
break;
case MATCH_ACCOUNTS_BY_ID:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_ACCOUNTS:
tableToChange = TABLE_ACCOUNTS;
notifyProviderAccountContentUri = true;
break;
case MATCH_ACCOUNT_STATUS:
changedItemId = url.getPathSegments().get(1);
// fall through
case MATCH_ACCOUNTS_STATUS:
tableToChange = TABLE_ACCOUNT_STATUS;
notifyProviderAccountContentUri = true;
break;
case MATCH_CONTACTS:
case MATCH_CONTACTS_BAREBONE:
tableToChange = TABLE_CONTACTS;
break;
case MATCH_CONTACTS_BY_PROVIDER:
tableToChange = TABLE_CONTACTS;
changedItemId = url.getPathSegments().get(2);
idColumnName = Imps.Contacts.ACCOUNT;
break;
case MATCH_CONTACT:
tableToChange = TABLE_CONTACTS;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_CONTACTS_BULK:
count = updateBulkContacts(values, userWhere);
// notify change using the "content://im/contacts" url,
// so the change will be observed by listeners interested
// in contacts changes.
if (count > 0) {
getContext().getContentResolver().notifyChange(Imps.Contacts.CONTENT_URI, null);
}
return count;
case MATCH_CONTACTLIST:
tableToChange = TABLE_CONTACT_LIST;
changedItemId = url.getPathSegments().get(1);
notifyContactListContentUri = true;
break;
case MATCH_CONTACTS_ETAGS:
tableToChange = TABLE_CONTACTS_ETAG;
break;
case MATCH_CONTACTS_ETAG:
tableToChange = TABLE_CONTACTS_ETAG;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_MESSAGES:
tableToChange = TABLE_MESSAGES;
break;
case MATCH_MESSAGES_BY_CONTACT:
tableToChange = TABLE_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
getContactId(db, accountStr, contact));
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGES_BY_THREAD_ID:
tableToChange = TABLE_MESSAGES;
try {
threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
notifyMessagesContentUri = true;
break;
case MATCH_MESSAGE:
tableToChange = TABLE_MESSAGES;
changedItemId = url.getPathSegments().get(1);
notifyMessagesContentUri = true;
break;
case MATCH_OTR_MESSAGES:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
break;
case MATCH_OTR_MESSAGES_BY_CONTACT:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
accountStr = decodeURLSegment(url.getPathSegments().get(1));
try {
account = Long.parseLong(accountStr);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
contact = decodeURLSegment(url.getPathSegments().get(2));
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
getContactId(db, accountStr, contact));
notifyMessagesByContactContentUri = true;
break;
case MATCH_OTR_MESSAGES_BY_THREAD_ID:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
try {
threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException();
}
appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
notifyMessagesByThreadIdContentUri = true;
break;
case MATCH_OTR_MESSAGES_BY_PACKET_ID:
packetId = decodeURLSegment(url.getPathSegments().get(1));
tableToChange = TABLE_IN_MEMORY_MESSAGES; // FIXME these should be going to memory but they do not
appendWhere(whereClause, Imps.Messages.PACKET_ID, "=", packetId);
notifyMessagesContentUri = true;
// Try updating OTR message
count += db.update(TABLE_IN_MEMORY_MESSAGES, values, whereClause.toString(), whereArgs);
break;
case MATCH_MESSAGES_BY_PACKET_ID:
packetId = decodeURLSegment(url.getPathSegments().get(1));
tableToChange = TABLE_MESSAGES; // FIXME these should be going to memory but they do not
appendWhere(whereClause, Imps.Messages.PACKET_ID, "=", packetId);
notifyMessagesContentUri = true;
// Try updating OTR message
count += db.update(TABLE_MESSAGES, values, whereClause.toString(), whereArgs);
break;
case MATCH_OTR_MESSAGE:
tableToChange = TABLE_IN_MEMORY_MESSAGES;
changedItemId = url.getPathSegments().get(1);
notifyMessagesContentUri = true;
break;
case MATCH_AVATARS:
tableToChange = TABLE_AVATARS;
break;
case MATCH_AVATAR:
tableToChange = TABLE_AVATARS;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_AVATAR_BY_PROVIDER:
tableToChange = TABLE_AVATARS;
changedItemId = url.getPathSegments().get(2);
idColumnName = Imps.Avatars.ACCOUNT;
break;
case MATCH_CHATS:
tableToChange = TABLE_CHATS;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_CHATS_ID:
tableToChange = TABLE_CHATS;
changedItemId = url.getPathSegments().get(1);
idColumnName = Imps.Chats.CONTACT_ID;
notifyProviderAccountContentUri = true; // For updating account stats in account list
break;
case MATCH_PRESENCE:
log("update presence: where='" + userWhere + "'");
tableToChange = TABLE_PRESENCE;
break;
case MATCH_PRESENCE_ID:
tableToChange = TABLE_PRESENCE;
changedItemId = url.getPathSegments().get(1);
idColumnName = Imps.Presence.CONTACT_ID;
break;
case MATCH_PRESENCE_BULK:
tableToChange = null;
count = updateBulkPresence(values, db, userWhere, whereArgs);
// notify change using the "content://im/contacts" url,
// so the change will be observed by listeners interested
// in contacts changes.
if (count > 0)
{
getContext().getContentResolver().notifyChange(Imps.Contacts.CONTENT_URI_CHAT_CONTACTS_BY, null);
getContext().getContentResolver().notifyChange(Imps.Contacts.CONTENT_URI, null);
notifyContactListContentUri = true;
}
break;
case MATCH_INVITATION:
tableToChange = TABLE_INVITATIONS;
changedItemId = url.getPathSegments().get(1);
break;
case MATCH_SESSIONS:
tableToChange = TABLE_SESSION_COOKIES;
break;
case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
tableToChange = TABLE_PROVIDER_SETTINGS;
String providerId = url.getPathSegments().get(1);
String name = url.getPathSegments().get(2);
if (values.containsKey(Imps.ProviderSettings.PROVIDER)
|| values.containsKey(Imps.ProviderSettings.NAME)) {
throw new SecurityException("Cannot override the value for provider|name");
}
appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", providerId);
appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", name);
break;
case MATCH_OUTGOING_RMQ_MESSAGES:
tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
break;
case MATCH_LAST_RMQ_ID:
tableToChange = TABLE_LAST_RMQ_ID;
break;
case MATCH_S2D_RMQ_IDS:
tableToChange = TABLE_S2D_RMQ_IDS;
break;
default:
throw new UnsupportedOperationException("Cannot update URL: " + url);
}
if (idColumnName == null) {
idColumnName = "_id";
}
if (changedItemId != null) {
appendWhere(whereClause, idColumnName, "=", changedItemId);
}
log("update " + url + " WHERE " + whereClause);
if (tableToChange != null)
count += db.update(tableToChange, values, whereClause.toString(), whereArgs);
if (count > 0) {
ContentResolver resolver = getContext().getContentResolver();
// In most case, we query contacts with presence and chats joined, thus
// we should also notify that contacts changes when presence or chats changed.
if (match == MATCH_CHATS || match == MATCH_CHATS_ID || match == MATCH_PRESENCE || match == MATCH_PRESENCE_BULK
|| match == MATCH_PRESENCE_ID || match == MATCH_CONTACTS_BAREBONE) {
resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
}
if (notifyMessagesContentUri) {
log("notify change for " + Imps.Messages.CONTENT_URI);
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
}
if (notifyMessagesByContactContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
}
if (notifyMessagesByThreadIdContentUri) {
resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
}
if (notifyContactListContentUri) {
resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
}
if (notifyProviderAccountContentUri) {
log("notify change for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
}
}
return count;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return openFileHelper(uri, mode);
}
private static void appendWhere(StringBuilder where, String columnName, String condition,
Object value) {
if (where.length() > 0) {
where.append(" AND ");
}
where.append(columnName).append(condition);
if (value != null) {
DatabaseUtils.appendValueToSql(where, value);
}
}
private static void appendWhere(StringBuilder where, String clause) {
if (where.length() > 0) {
where.append(" AND ");
}
where.append(clause);
}
private static String decodeURLSegment(String segment) {
try {
return URLDecoder.decode(segment, "UTF-8");
} catch (UnsupportedEncodingException e) {
// impossible
return segment;
}
}
static void log(String message) {
LogCleaner.debug(LOG_TAG, message);
}
}