Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.im.provider;
     18 
     19 import android.content.ContentProvider;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.content.UriMatcher;
     23 import android.content.ContentResolver;
     24 import android.database.Cursor;
     25 import android.database.DatabaseUtils;
     26 import android.database.sqlite.SQLiteConstraintException;
     27 import android.database.sqlite.SQLiteDatabase;
     28 import android.database.sqlite.SQLiteOpenHelper;
     29 import android.database.sqlite.SQLiteQueryBuilder;
     30 import android.net.Uri;
     31 import android.os.ParcelFileDescriptor;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 
     35 
     36 import java.io.FileNotFoundException;
     37 import java.io.UnsupportedEncodingException;
     38 import java.net.URLDecoder;
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 
     42 /**
     43  * A content provider for IM
     44  */
     45 public class ImpsProvider extends ContentProvider {
     46     private static final String LOG_TAG = "imProvider";
     47     private static final boolean DBG = false;
     48 
     49     private static final String AUTHORITY = "imps";
     50 
     51     private static final String TABLE_ACCOUNTS = "accounts";
     52     private static final String TABLE_PROVIDERS = "providers";
     53     private static final String TABLE_PROVIDER_SETTINGS = "providerSettings";
     54 
     55     private static final String TABLE_CONTACTS = "contacts";
     56     private static final String TABLE_CONTACTS_ETAG = "contactsEtag";
     57     private static final String TABLE_BLOCKED_LIST = "blockedList";
     58     private static final String TABLE_CONTACT_LIST = "contactList";
     59     private static final String TABLE_INVITATIONS = "invitations";
     60     private static final String TABLE_GROUP_MEMBERS = "groupMembers";
     61     private static final String TABLE_PRESENCE = "presence";
     62     private static final String USERNAME = "username";
     63     private static final String TABLE_CHATS = "chats";
     64     private static final String TABLE_AVATARS = "avatars";
     65     private static final String TABLE_SESSION_COOKIES = "sessionCookies";
     66     private static final String TABLE_MESSAGES = "messages";
     67     private static final String TABLE_IN_MEMORY_MESSAGES = "inMemoryMessages";
     68     private static final String TABLE_ACCOUNT_STATUS = "accountStatus";
     69     private static final String TABLE_BRANDING_RESOURCE_MAP_CACHE = "brandingResMapCache";
     70 
     71     // tables for mcs and rmq
     72     private static final String TABLE_OUTGOING_RMQ_MESSAGES = "outgoingRmqMessages";
     73     private static final String TABLE_LAST_RMQ_ID = "lastrmqid";
     74     private static final String TABLE_S2D_RMQ_IDS = "s2dRmqIds";
     75 
     76 
     77     private static final String DATABASE_NAME = "imps.db";
     78     private static final int DATABASE_VERSION = 1;
     79 
     80     protected static final int MATCH_PROVIDERS = 1;
     81     protected static final int MATCH_PROVIDERS_BY_ID = 2;
     82     protected static final int MATCH_PROVIDERS_WITH_ACCOUNT = 3;
     83     protected static final int MATCH_ACCOUNTS = 10;
     84     protected static final int MATCH_ACCOUNTS_BY_ID = 11;
     85     protected static final int MATCH_CONTACTS = 18;
     86     protected static final int MATCH_CONTACTS_JOIN_PRESENCE = 19;
     87     protected static final int MATCH_CONTACTS_BAREBONE = 20;
     88     protected static final int MATCH_CHATTING_CONTACTS = 21;
     89     protected static final int MATCH_CONTACTS_BY_PROVIDER = 22;
     90     protected static final int MATCH_CHATTING_CONTACTS_BY_PROVIDER = 23;
     91     protected static final int MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER = 24;
     92     protected static final int MATCH_ONLINE_CONTACTS_BY_PROVIDER = 25;
     93     protected static final int MATCH_OFFLINE_CONTACTS_BY_PROVIDER = 26;
     94     protected static final int MATCH_CONTACT = 27;
     95     protected static final int MATCH_CONTACTS_BULK = 28;
     96     protected static final int MATCH_ONLINE_CONTACT_COUNT = 30;
     97     protected static final int MATCH_BLOCKED_CONTACTS = 31;
     98     protected static final int MATCH_CONTACTLISTS = 32;
     99     protected static final int MATCH_CONTACTLISTS_BY_PROVIDER = 33;
    100     protected static final int MATCH_CONTACTLIST = 34;
    101     protected static final int MATCH_BLOCKEDLIST = 35;
    102     protected static final int MATCH_BLOCKEDLIST_BY_PROVIDER = 36;
    103     protected static final int MATCH_CONTACTS_ETAGS = 37;
    104     protected static final int MATCH_CONTACTS_ETAG = 38;
    105     protected static final int MATCH_PRESENCE = 40;
    106     protected static final int MATCH_PRESENCE_ID = 41;
    107     protected static final int MATCH_PRESENCE_BY_ACCOUNT = 42;
    108     protected static final int MATCH_PRESENCE_SEED_BY_ACCOUNT = 43;
    109     protected static final int MATCH_PRESENCE_BULK = 44;
    110 
    111     protected static final int MATCH_MESSAGES = 50;
    112     protected static final int MATCH_MESSAGES_BY_CONTACT = 51;
    113     protected static final int MATCH_MESSAGES_BY_THREAD_ID = 52;
    114     protected static final int MATCH_MESSAGES_BY_PROVIDER = 53;
    115     protected static final int MATCH_MESSAGES_BY_ACCOUNT = 54;
    116     protected static final int MATCH_MESSAGE = 55;
    117     protected static final int MATCH_OTR_MESSAGES = 56;
    118     protected static final int MATCH_OTR_MESSAGES_BY_CONTACT = 57;
    119     protected static final int MATCH_OTR_MESSAGES_BY_THREAD_ID = 58;
    120     protected static final int MATCH_OTR_MESSAGES_BY_PROVIDER = 59;
    121     protected static final int MATCH_OTR_MESSAGES_BY_ACCOUNT = 60;
    122     protected static final int MATCH_OTR_MESSAGE = 61;
    123 
    124     protected static final int MATCH_GROUP_MEMBERS = 65;
    125     protected static final int MATCH_GROUP_MEMBERS_BY_GROUP = 66;
    126     protected static final int MATCH_AVATARS = 70;
    127     protected static final int MATCH_AVATAR = 71;
    128     protected static final int MATCH_AVATAR_BY_PROVIDER = 72;
    129     protected static final int MATCH_CHATS = 80;
    130     protected static final int MATCH_CHATS_BY_ACCOUNT = 81;
    131     protected static final int MATCH_CHATS_ID = 82;
    132     protected static final int MATCH_SESSIONS = 83;
    133     protected static final int MATCH_SESSIONS_BY_PROVIDER = 84;
    134     protected static final int MATCH_PROVIDER_SETTINGS = 90;
    135     protected static final int MATCH_PROVIDER_SETTINGS_BY_ID = 91;
    136     protected static final int MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME = 92;
    137     protected static final int MATCH_INVITATIONS = 100;
    138     protected static final int MATCH_INVITATION  = 101;
    139     protected static final int MATCH_ACCOUNTS_STATUS = 104;
    140     protected static final int MATCH_ACCOUNT_STATUS = 105;
    141     protected static final int MATCH_BRANDING_RESOURCE_MAP_CACHE = 106;
    142 
    143     // mcs url matcher
    144     protected static final int MATCH_OUTGOING_RMQ_MESSAGES = 200;
    145     protected static final int MATCH_OUTGOING_RMQ_MESSAGE = 201;
    146     protected static final int MATCH_OUTGOING_HIGHEST_RMQ_ID = 202;
    147     protected static final int MATCH_LAST_RMQ_ID = 203;
    148     protected static final int MATCH_S2D_RMQ_IDS = 204;
    149 
    150 
    151     protected final UriMatcher mUrlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    152     private final String mTransientDbName;
    153 
    154     private static final HashMap<String, String> sProviderAccountsProjectionMap;
    155     private static final HashMap<String, String> sContactsProjectionMap;
    156     private static final HashMap<String, String> sContactListProjectionMap;
    157     private static final HashMap<String, String> sBlockedListProjectionMap;
    158     private static final HashMap<String, String> sMessagesProjectionMap;
    159     private static final HashMap<String, String> sInMemoryMessagesProjectionMap;
    160 
    161 
    162     private static final String PROVIDER_JOIN_ACCOUNT_TABLE =
    163             "providers LEFT OUTER JOIN accounts ON " +
    164                     "(providers._id = accounts.provider AND accounts.active = 1) " +
    165                     "LEFT OUTER JOIN accountStatus ON (accounts._id = accountStatus.account)";
    166 
    167 
    168     private static final String CONTACT_JOIN_PRESENCE_TABLE =
    169             "contacts LEFT OUTER JOIN presence ON (contacts._id = presence.contact_id)";
    170 
    171     private static final String CONTACT_JOIN_PRESENCE_CHAT_TABLE =
    172             CONTACT_JOIN_PRESENCE_TABLE +
    173                     " LEFT OUTER JOIN chats ON (contacts._id = chats.contact_id)";
    174 
    175     private static final String CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE =
    176             CONTACT_JOIN_PRESENCE_CHAT_TABLE +
    177                     " LEFT OUTER JOIN avatars ON (contacts.username = avatars.contact" +
    178                     " AND contacts.account = avatars.account_id)";
    179 
    180     private static final String BLOCKEDLIST_JOIN_AVATAR_TABLE =
    181             "blockedList LEFT OUTER JOIN avatars ON (blockedList.username = avatars.contact" +
    182                     " AND blockedList.account = avatars.account_id)";
    183 
    184     private static final String MESSAGE_JOIN_CONTACT_TABLE =
    185             "messages LEFT OUTER JOIN contacts ON (contacts._id = messages.thread_id)";
    186 
    187     private static final String IN_MEMORY_MESSAGES_JOIN_CONTACT_TABLE =
    188             "inMemoryMessages LEFT OUTER JOIN contacts ON " +
    189                 "(contacts._id = inMemoryMessages.thread_id)";
    190 
    191     /**
    192      * The where clause for filtering out blocked contacts
    193      */
    194     private static final String NON_BLOCKED_CONTACTS_WHERE_CLAUSE = "("
    195         + Imps.Contacts.TYPE + " IS NULL OR "
    196         + Imps.Contacts.TYPE + "!="
    197         + String.valueOf(Imps.Contacts.TYPE_BLOCKED)
    198         + ")";
    199 
    200     private static final String BLOCKED_CONTACTS_WHERE_CLAUSE =
    201         "(contacts." + Imps.Contacts.TYPE + "=" + Imps.Contacts.TYPE_BLOCKED + ")";
    202 
    203     private static final String CONTACT_ID = TABLE_CONTACTS + '.' + Imps.Contacts._ID;
    204     private static final String PRESENCE_CONTACT_ID = TABLE_PRESENCE + '.' + Imps.Presence.CONTACT_ID;
    205 
    206     protected SQLiteOpenHelper mOpenHelper;
    207     private final String mDatabaseName;
    208     private final int mDatabaseVersion;
    209 
    210     private final String[] BACKFILL_PROJECTION = {
    211         Imps.Chats._ID, Imps.Chats.SHORTCUT, Imps.Chats.LAST_MESSAGE_DATE
    212     };
    213 
    214     private final String[] FIND_SHORTCUT_PROJECTION = {
    215         Imps.Chats._ID, Imps.Chats.SHORTCUT
    216     };
    217 
    218     // contact id query projection
    219     private static final String[] CONTACT_ID_PROJECTION = new String[] {
    220             Imps.Contacts._ID,    // 0
    221     };
    222     private static final int CONTACT_ID_COLUMN = 0;
    223 
    224     // contact id query selection for "seed presence" operation
    225     private static final String CONTACTS_WITH_NO_PRESENCE_SELECTION =
    226             Imps.Contacts.ACCOUNT + "=?" + " AND " + Imps.Contacts._ID +
    227                     " in (select " + CONTACT_ID + " from " + TABLE_CONTACTS +
    228                     " left outer join " + TABLE_PRESENCE + " on " + CONTACT_ID + '=' +
    229                     PRESENCE_CONTACT_ID + " where " + PRESENCE_CONTACT_ID + " IS NULL)";
    230 
    231     // contact id query selection args 1
    232     private String[] mQueryContactIdSelectionArgs1 = new String[1];
    233 
    234     // contact id query selection for getContactId()
    235     private static final String CONTACT_ID_QUERY_SELECTION =
    236             Imps.Contacts.ACCOUNT + "=? AND " + Imps.Contacts.USERNAME + "=?";
    237 
    238     // contact id query selection args 2
    239     private String[] mQueryContactIdSelectionArgs2 = new String[2];
    240 
    241 
    242 
    243     private class DatabaseHelper extends SQLiteOpenHelper {
    244 
    245         DatabaseHelper(Context context) {
    246             super(context, mDatabaseName, null, mDatabaseVersion);
    247         }
    248 
    249         @Override
    250         public void onCreate(SQLiteDatabase db) {
    251 
    252             if (DBG) log("DatabaseHelper.onCreate");
    253 
    254             db.execSQL("CREATE TABLE " + TABLE_PROVIDERS + " (" +
    255                     "_id INTEGER PRIMARY KEY," +
    256                     "name TEXT," +       // eg AIM
    257                     "fullname TEXT," +   // eg AOL Instance Messenger
    258                     "category TEXT," +   // a category used for forming intent
    259                     "signup_url TEXT" +  // web url to visit to create a new account
    260                     ");");
    261 
    262             db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " (" +
    263                     "_id INTEGER PRIMARY KEY," +
    264                     "name TEXT," +
    265                     "provider INTEGER," +
    266                     "username TEXT," +
    267                     "pw TEXT," +
    268                     "active INTEGER NOT NULL DEFAULT 0," +
    269                     "locked INTEGER NOT NULL DEFAULT 0," +
    270                     "keep_signed_in INTEGER NOT NULL DEFAULT 0," +
    271                     "last_login_state INTEGER NOT NULL DEFAULT 0," +
    272                     "UNIQUE (provider, username)" +
    273                     ");");
    274 
    275             createContactsTables(db);
    276             createMessageChatTables(db, true /* create show_ts column */);
    277 
    278             db.execSQL("CREATE TABLE " + TABLE_AVATARS + " (" +
    279                     "_id INTEGER PRIMARY KEY," +
    280                     "contact TEXT," +
    281                     "provider_id INTEGER," +
    282                     "account_id INTEGER," +
    283                     "hash TEXT," +
    284                     "data BLOB," +     // raw image data
    285                     "UNIQUE (account_id, contact)" +
    286                     ");");
    287 
    288             db.execSQL("CREATE TABLE " + TABLE_PROVIDER_SETTINGS + " (" +
    289                     "_id INTEGER PRIMARY KEY," +
    290                     "provider INTEGER," +
    291                     "name TEXT," +
    292                     "value TEXT," +
    293                     "UNIQUE (provider, name)" +
    294                     ");");
    295 
    296             db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " (" +
    297                     "_id INTEGER PRIMARY KEY," +
    298                     "provider_id INTEGER," +
    299                     "app_res_id INTEGER," +
    300                     "plugin_res_id INTEGER" +
    301                     ");");
    302 
    303             // clean up account specific data when an account is deleted.
    304             db.execSQL("CREATE TRIGGER account_cleanup " +
    305                     "DELETE ON " + TABLE_ACCOUNTS +
    306                     " BEGIN " +
    307                         "DELETE FROM " + TABLE_AVATARS + " WHERE account_id= OLD._id;" +
    308                     "END");
    309 
    310             // add a database trigger to clean up associated provider settings
    311             // while deleting a provider
    312             db.execSQL("CREATE TRIGGER provider_cleanup " +
    313                     "DELETE ON " + TABLE_PROVIDERS +
    314                     " BEGIN " +
    315                         "DELETE FROM " + TABLE_PROVIDER_SETTINGS + " WHERE provider= OLD._id;" +
    316                     "END");
    317 
    318             // the following are tables for mcs
    319             db.execSQL("create TABLE " + TABLE_OUTGOING_RMQ_MESSAGES + " (" +
    320                     "_id INTEGER PRIMARY KEY," +
    321                     "rmq_id INTEGER," +
    322                     "type INTEGER," +
    323                     "ts INTEGER," +
    324                     "data TEXT" +
    325                     ");");
    326 
    327             db.execSQL("create TABLE " + TABLE_LAST_RMQ_ID + " (" +
    328                     "_id INTEGER PRIMARY KEY," +
    329                     "rmq_id INTEGER" +
    330                     ");");
    331 
    332             db.execSQL("create TABLE " + TABLE_S2D_RMQ_IDS + " (" +
    333                     "_id INTEGER PRIMARY KEY," +
    334                     "rmq_id INTEGER" +
    335                     ");");
    336         }
    337 
    338         @Override
    339         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    340             Log.d(LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
    341 
    342             switch (oldVersion) {
    343                 case 43:    // this is the db version shipped in Dream 1.0
    344                     // no-op: no schema changed from 43 to 44. The db version was changed to flush
    345                     // old provider settings, so new provider setting (including new name/value
    346                     // pairs) could be inserted by the plugins.
    347 
    348                     // follow thru.
    349                 case 44:
    350                     if (newVersion <= 44) {
    351                         return;
    352                     }
    353 
    354                     db.beginTransaction();
    355                     try {
    356                         // add category column to the providers table
    357                         db.execSQL("ALTER TABLE " + TABLE_PROVIDERS + " ADD COLUMN category TEXT;");
    358                         // add otr column to the contacts table
    359                         db.execSQL("ALTER TABLE " + TABLE_CONTACTS + " ADD COLUMN otr INTEGER;");
    360 
    361                         db.setTransactionSuccessful();
    362                     } catch (Throwable ex) {
    363                         Log.e(LOG_TAG, ex.getMessage(), ex);
    364                         break; // force to destroy all old data;
    365                     } finally {
    366                         db.endTransaction();
    367                     }
    368 
    369                 case 45:
    370                     if (newVersion <= 45) {
    371                         return;
    372                     }
    373 
    374                     db.beginTransaction();
    375                     try {
    376                         // add an otr_etag column to contact etag table
    377                         db.execSQL(
    378                                 "ALTER TABLE " + TABLE_CONTACTS_ETAG + " ADD COLUMN otr_etag TEXT;");
    379                         db.setTransactionSuccessful();
    380                     } catch (Throwable ex) {
    381                         Log.e(LOG_TAG, ex.getMessage(), ex);
    382                         break; // force to destroy all old data;
    383                     } finally {
    384                         db.endTransaction();
    385                     }
    386 
    387                 case 46:
    388                     if (newVersion <= 46) {
    389                         return;
    390                     }
    391 
    392                     db.beginTransaction();
    393                     try {
    394                         // add branding resource map cache table
    395                         db.execSQL("create TABLE " + TABLE_BRANDING_RESOURCE_MAP_CACHE + " (" +
    396                                 "_id INTEGER PRIMARY KEY," +
    397                                 "provider_id INTEGER," +
    398                                 "app_res_id INTEGER," +
    399                                 "plugin_res_id INTEGER" +
    400                                 ");");
    401                         db.setTransactionSuccessful();
    402                     } catch (Throwable ex) {
    403                         Log.e(LOG_TAG, ex.getMessage(), ex);
    404                         break; // force to destroy all old data;
    405                     } finally {
    406                         db.endTransaction();
    407                     }
    408 
    409                 case 47:
    410                     if (newVersion <= 47) {
    411                         return;
    412                     }
    413 
    414                     db.beginTransaction();
    415                     try {
    416                         // when upgrading from version 47, don't create the show_ts column
    417                         // here. The upgrade step in 51 will add the show_ts column to the
    418                         // messages table. If we created the messages table with show_ts here,
    419                         // we'll get a duplicate column error later.
    420                         createMessageChatTables(db, false /* don't create show_ts column */);
    421                         db.setTransactionSuccessful();
    422                     } catch (Throwable ex) {
    423                         Log.e(LOG_TAG, ex.getMessage(), ex);
    424                         break; // force to destroy all old data;
    425                     } finally {
    426                         db.endTransaction();
    427                     }
    428 
    429                     // fall thru.
    430 
    431                 case 48:
    432                 case 49:
    433                 case 50:
    434                     if (newVersion <= 50) {
    435                         return;
    436                     }
    437 
    438                     db.beginTransaction();
    439                     try {
    440                         // add rmq2 s2d ids table
    441                         db.execSQL("create TABLE " + TABLE_S2D_RMQ_IDS + " (" +
    442                                 "_id INTEGER PRIMARY KEY," +
    443                                 "rmq_id INTEGER" +
    444                                 ");");
    445                         db.setTransactionSuccessful();
    446                     } catch (Throwable ex) {
    447                         Log.e(LOG_TAG, ex.getMessage(), ex);
    448                         break; // force to destroy all old data;
    449                     } finally {
    450                         db.endTransaction();
    451                     }
    452 
    453                 case 51:
    454                     if (newVersion <= 51) {
    455                         return;
    456                     }
    457 
    458                     db.beginTransaction();
    459                     try {
    460                         db.execSQL(
    461                                 "ALTER TABLE " + TABLE_MESSAGES + " ADD COLUMN show_ts INTEGER;");
    462                         db.setTransactionSuccessful();
    463                     } catch (Throwable ex) {
    464                         Log.e(LOG_TAG, ex.getMessage(), ex);
    465                         break; // force to destroy all old data;
    466                     } finally {
    467                         db.endTransaction();
    468                     }
    469 
    470                     return;
    471             }
    472 
    473             Log.w(LOG_TAG, "Couldn't upgrade db to " + newVersion + ". Destroying old data.");
    474             destroyOldTables(db);
    475             onCreate(db);
    476         }
    477 
    478         private void destroyOldTables(SQLiteDatabase db) {
    479             db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDERS);
    480             db.execSQL("DROP TABLE IF EXISTS " + TABLE_ACCOUNTS);
    481             db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACT_LIST);
    482             db.execSQL("DROP TABLE IF EXISTS " + TABLE_BLOCKED_LIST);
    483             db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS);
    484             db.execSQL("DROP TABLE IF EXISTS " + TABLE_CONTACTS_ETAG);
    485             db.execSQL("DROP TABLE IF EXISTS " + TABLE_AVATARS);
    486             db.execSQL("DROP TABLE IF EXISTS " + TABLE_PROVIDER_SETTINGS);
    487             db.execSQL("DROP TABLE IF EXISTS " + TABLE_BRANDING_RESOURCE_MAP_CACHE);
    488             db.execSQL("DROP TABLE IF EXISTS " + TABLE_MESSAGES);
    489             db.execSQL("DROP TABLE IF EXISTS " + TABLE_CHATS);
    490 
    491             // mcs/rmq stuff
    492             db.execSQL("DROP TABLE IF EXISTS " + TABLE_OUTGOING_RMQ_MESSAGES);
    493             db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_RMQ_ID);
    494             db.execSQL("DROP TABLE IF EXISTS " + TABLE_S2D_RMQ_IDS);
    495         }
    496 
    497         private void createContactsTables(SQLiteDatabase db) {
    498             if (DBG) log("createContactsTables");
    499 
    500             StringBuilder buf = new StringBuilder();
    501             String contactsTableName = TABLE_CONTACTS;
    502 
    503             // creating the "contacts" table
    504             buf.append("CREATE TABLE IF NOT EXISTS ");
    505             buf.append(contactsTableName);
    506             buf.append(" (");
    507             buf.append("_id INTEGER PRIMARY KEY,");
    508             buf.append("username TEXT,");
    509             buf.append("nickname TEXT,");
    510 
    511             buf.append("provider INTEGER,");
    512             buf.append("account INTEGER,");
    513             buf.append("contactList INTEGER,");
    514             buf.append("type INTEGER,");
    515             buf.append("subscriptionStatus INTEGER,");
    516             buf.append("subscriptionType INTEGER,");
    517 
    518             // the following are derived from Google Contact Extension, we don't include all
    519             // the attributes, just the ones we can use.
    520             // (see http://code.google.com/apis/talk/jep_extensions/roster_attributes.html)
    521             //
    522             // qc: quick contact (derived from message count)
    523             // rejected: if the contact has ever been rejected by the user
    524             buf.append("qc INTEGER,");
    525             buf.append("rejected INTEGER,");
    526 
    527             // Off the record status
    528             buf.append("otr INTEGER");
    529 
    530             buf.append(");");
    531 
    532             db.execSQL(buf.toString());
    533 
    534             buf.delete(0, buf.length());
    535 
    536             // creating contact etag table
    537             buf.append("CREATE TABLE IF NOT EXISTS ");
    538             buf.append(TABLE_CONTACTS_ETAG);
    539             buf.append(" (");
    540             buf.append("_id INTEGER PRIMARY KEY,");
    541             buf.append("etag TEXT,");
    542             buf.append("otr_etag TEXT,");
    543             buf.append("account INTEGER UNIQUE");
    544             buf.append(");");
    545 
    546             db.execSQL(buf.toString());
    547 
    548             buf.delete(0, buf.length());
    549 
    550             // creating the "contactList" table
    551             buf.append("CREATE TABLE IF NOT EXISTS ");
    552             buf.append(TABLE_CONTACT_LIST);
    553             buf.append(" (");
    554             buf.append("_id INTEGER PRIMARY KEY,");
    555             buf.append("name TEXT,");
    556             buf.append("provider INTEGER,");
    557             buf.append("account INTEGER");
    558             buf.append(");");
    559 
    560             db.execSQL(buf.toString());
    561 
    562             buf.delete(0, buf.length());
    563 
    564             // creating the "blockedList" table
    565             buf.append("CREATE TABLE IF NOT EXISTS ");
    566             buf.append(TABLE_BLOCKED_LIST);
    567             buf.append(" (");
    568             buf.append("_id INTEGER PRIMARY KEY,");
    569             buf.append("username TEXT,");
    570             buf.append("nickname TEXT,");
    571             buf.append("provider INTEGER,");
    572             buf.append("account INTEGER");
    573             buf.append(");");
    574 
    575             db.execSQL(buf.toString());
    576         }
    577 
    578         private void createMessageChatTables(SQLiteDatabase db,
    579                                              boolean addShowTsColumnForMessagesTable) {
    580             if (DBG) log("createMessageChatTables");
    581 
    582             // message table
    583             StringBuilder buf = new StringBuilder();
    584             buf.append("CREATE TABLE IF NOT EXISTS ");
    585             buf.append(TABLE_MESSAGES);
    586             buf.append(" (_id INTEGER PRIMARY KEY,");
    587             buf.append("thread_id INTEGER,");
    588             buf.append("nickname TEXT,");
    589             buf.append("body TEXT,");
    590             buf.append("date INTEGER,");
    591             buf.append("type INTEGER,");
    592             buf.append("packet_id TEXT UNIQUE,");
    593             buf.append("err_code INTEGER NOT NULL DEFAULT 0,");
    594             buf.append("err_msg TEXT,");
    595             buf.append("is_muc INTEGER");
    596 
    597             if (addShowTsColumnForMessagesTable) {
    598                 buf.append(",show_ts INTEGER");
    599             }
    600 
    601             buf.append(");");
    602 
    603             String sqlStatement = buf.toString();
    604 
    605             if (DBG) log("create message table: " + sqlStatement);
    606             db.execSQL(sqlStatement);
    607 
    608             buf.delete(0, buf.length());
    609             buf.append("CREATE TABLE IF NOT EXISTS ");
    610             buf.append(TABLE_CHATS);
    611             buf.append(" (_id INTEGER PRIMARY KEY,");
    612             buf.append("contact_id INTEGER UNIQUE,");
    613             buf.append("jid_resource TEXT,"); // the JID resource for the user, for non-group chats
    614             buf.append("groupchat INTEGER,"); // 1 if group chat, 0 if not TODO: remove this column
    615             buf.append("last_unread_message TEXT,"); // the last unread message
    616             buf.append("last_message_date INTEGER,"); // in seconds
    617             buf.append("unsent_composed_message TEXT,"); // a composed, but not sent message
    618             buf.append("shortcut INTEGER);"); // which of 10 slots (if any) this chat occupies
    619 
    620             // chat sessions, including single person chats and group chats
    621             sqlStatement = buf.toString();
    622 
    623             if (DBG) log("create chat table: " + sqlStatement);
    624             db.execSQL(sqlStatement);
    625 
    626             buf.delete(0, buf.length());
    627             buf.append("CREATE TRIGGER IF NOT EXISTS contact_cleanup ");
    628             buf.append("DELETE ON contacts ");
    629             buf.append("BEGIN ");
    630             buf.append("DELETE FROM ").append(TABLE_CHATS).append(" WHERE contact_id = OLD._id;");
    631             buf.append("DELETE FROM ").append(TABLE_MESSAGES).append(" WHERE thread_id = OLD._id;");
    632             buf.append("END");
    633 
    634             sqlStatement = buf.toString();
    635 
    636             if (DBG) log("create trigger: " + sqlStatement);
    637             db.execSQL(sqlStatement);
    638         }
    639 
    640         private void createInMemoryMessageTables(SQLiteDatabase db, String tablePrefix) {
    641             String tableName = (tablePrefix != null) ?
    642                     tablePrefix+TABLE_IN_MEMORY_MESSAGES : TABLE_IN_MEMORY_MESSAGES;
    643 
    644             db.execSQL("CREATE TABLE IF NOT EXISTS " + tableName + " (" +
    645                     "_id INTEGER PRIMARY KEY," +
    646                     "thread_id INTEGER," +
    647                     "nickname TEXT," +
    648                     "body TEXT," +
    649                     "date INTEGER," +    // in millisec
    650                     "type INTEGER," +
    651                     "packet_id TEXT UNIQUE," +
    652                     "err_code INTEGER NOT NULL DEFAULT 0," +
    653                     "err_msg TEXT," +
    654                     "is_muc INTEGER," +
    655                     "show_ts INTEGER" +
    656                     ");");
    657 
    658         }
    659 
    660         @Override
    661         public void onOpen(SQLiteDatabase db) {
    662             if (db.isReadOnly()) {
    663                 Log.w(LOG_TAG, "ImProvider database opened in read only mode.");
    664                 Log.w(LOG_TAG, "Transient tables not created.");
    665                 return;
    666             }
    667 
    668             if (DBG) log("##### createTransientTables");
    669 
    670             // Create transient tables
    671             String cpDbName;
    672             db.execSQL("ATTACH DATABASE ':memory:' AS " + mTransientDbName + ";");
    673             cpDbName = mTransientDbName + ".";
    674 
    675             // in-memory message table
    676             createInMemoryMessageTables(db, cpDbName);
    677 
    678             // presence
    679             db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_PRESENCE + " ("+
    680                     "_id INTEGER PRIMARY KEY," +
    681                     "contact_id INTEGER UNIQUE," +
    682                     "jid_resource TEXT," +  // jid resource for the presence
    683                     "client_type INTEGER," + // client type
    684                     "priority INTEGER," +   // presence priority (XMPP)
    685                     "mode INTEGER," +       // presence mode
    686                     "status TEXT" +         // custom status
    687                     ");");
    688 
    689             // group chat invitations
    690             db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_INVITATIONS + " (" +
    691                     "_id INTEGER PRIMARY KEY," +
    692                     "providerId INTEGER," +
    693                     "accountId INTEGER," +
    694                     "inviteId TEXT," +
    695                     "sender TEXT," +
    696                     "groupName TEXT," +
    697                     "note TEXT," +
    698                     "status INTEGER" +
    699                     ");");
    700 
    701             // group chat members
    702             db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_GROUP_MEMBERS + " (" +
    703                     "_id INTEGER PRIMARY KEY," +
    704                     "groupId INTEGER," +
    705                     "username TEXT," +
    706                     "nickname TEXT" +
    707                     ");");
    708 
    709             db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_ACCOUNT_STATUS + " (" +
    710                     "_id INTEGER PRIMARY KEY," +
    711                     "account INTEGER UNIQUE," +
    712                     "presenceStatus INTEGER," +
    713                     "connStatus INTEGER" +
    714                     ");"
    715             );
    716 
    717             /* when we moved the contact table out of transient_db and into the main db, the
    718                presence and groupchat cleanup triggers don't work anymore. It seems we can't
    719                create triggers that reference objects in a different database!
    720 
    721             // Insert a default presence for newly inserted contact
    722             db.execSQL("CREATE TRIGGER IF NOT EXISTS contact_create_presence " +
    723                     "AFTER INSERT ON " + contactsTableName +
    724                         " WHEN NEW.type != " + Im.Contacts.TYPE_GROUP +
    725                         " BEGIN " +
    726                             "INSERT INTO presence (contact_id) VALUES (NEW._id);" +
    727                         " END");
    728 
    729             // Remove the presence when the contact is removed.
    730             db.execSQL("CREATE TRIGGER IF NOT EXISTS contact_presence_cleanup " +
    731                     "DELETE ON " + contactsTableName +
    732                        " BEGIN " +
    733                            "DELETE FROM presence WHERE contact_id = OLD._id;" +
    734                        "END");
    735 
    736             // Cleans up group members and group messages when a group chat is deleted
    737             db.execSQL("CREATE TRIGGER IF NOT EXISTS " + cpDbName + "group_cleanup " +
    738                     "DELETE ON " + cpDbName + contactsTableName +
    739                        " FOR EACH ROW WHEN OLD.type = " + Im.Contacts.TYPE_GROUP +
    740                        " BEGIN " +
    741                            "DELETE FROM groupMembers WHERE groupId = OLD._id;" +
    742                            "DELETE FROM groupMessages WHERE groupId = OLD._id;" +
    743                        " END");
    744             */
    745 
    746             // only store the session cookies in memory right now. This means
    747             // that we don't persist them across device reboot
    748             db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + TABLE_SESSION_COOKIES + " ("+
    749                     "_id INTEGER PRIMARY KEY," +
    750                     "provider INTEGER," +
    751                     "account INTEGER," +
    752                     "name TEXT," +
    753                     "value TEXT" +
    754                     ");");
    755 
    756         }
    757     }
    758 
    759     static {
    760         sProviderAccountsProjectionMap = new HashMap<String, String>();
    761         sProviderAccountsProjectionMap.put(Imps.Provider._ID,
    762                 "providers._id AS _id");
    763         sProviderAccountsProjectionMap.put(Imps.Provider._COUNT,
    764                 "COUNT(*) AS _account");
    765         sProviderAccountsProjectionMap.put(Imps.Provider.NAME,
    766                 "providers.name AS name");
    767         sProviderAccountsProjectionMap.put(Imps.Provider.FULLNAME,
    768                 "providers.fullname AS fullname");
    769         sProviderAccountsProjectionMap.put(Imps.Provider.CATEGORY,
    770                 "providers.category AS category");
    771         sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_ID,
    772                 "accounts._id AS account_id");
    773         sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_USERNAME,
    774                 "accounts.username AS account_username");
    775         sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_PW,
    776                 "accounts.pw AS account_pw");
    777         sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_LOCKED,
    778                 "accounts.locked AS account_locked");
    779         sProviderAccountsProjectionMap.put(Imps.Provider.ACTIVE_ACCOUNT_KEEP_SIGNED_IN,
    780                 "accounts.keep_signed_in AS account_keepSignedIn");
    781         sProviderAccountsProjectionMap.put(Imps.Provider.ACCOUNT_PRESENCE_STATUS,
    782                 "accountStatus.presenceStatus AS account_presenceStatus");
    783         sProviderAccountsProjectionMap.put(Imps.Provider.ACCOUNT_CONNECTION_STATUS,
    784                 "accountStatus.connStatus AS account_connStatus");
    785 
    786         // contacts projection map
    787         sContactsProjectionMap = new HashMap<String, String>();
    788 
    789         // Base column
    790         sContactsProjectionMap.put(Imps.Contacts._ID, "contacts._id AS _id");
    791         sContactsProjectionMap.put(Imps.Contacts._COUNT, "COUNT(*) AS _count");
    792 
    793         // contacts column
    794         sContactsProjectionMap.put(Imps.Contacts._ID, "contacts._id as _id");
    795         sContactsProjectionMap.put(Imps.Contacts.USERNAME, "contacts.username as username");
    796         sContactsProjectionMap.put(Imps.Contacts.NICKNAME, "contacts.nickname as nickname");
    797         sContactsProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider as provider");
    798         sContactsProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account as account");
    799         sContactsProjectionMap.put(Imps.Contacts.CONTACTLIST, "contacts.contactList as contactList");
    800         sContactsProjectionMap.put(Imps.Contacts.TYPE, "contacts.type as type");
    801         sContactsProjectionMap.put(Imps.Contacts.SUBSCRIPTION_STATUS,
    802                 "contacts.subscriptionStatus as subscriptionStatus");
    803         sContactsProjectionMap.put(Imps.Contacts.SUBSCRIPTION_TYPE,
    804                 "contacts.subscriptionType as subscriptionType");
    805         sContactsProjectionMap.put(Imps.Contacts.QUICK_CONTACT, "contacts.qc as qc");
    806         sContactsProjectionMap.put(Imps.Contacts.REJECTED, "contacts.rejected as rejected");
    807 
    808         // Presence columns
    809         sContactsProjectionMap.put(Imps.Presence.CONTACT_ID,
    810                 "presence.contact_id AS contact_id");
    811         sContactsProjectionMap.put(Imps.Contacts.PRESENCE_STATUS,
    812                 "presence.mode AS mode");
    813         sContactsProjectionMap.put(Imps.Contacts.PRESENCE_CUSTOM_STATUS,
    814                 "presence.status AS status");
    815         sContactsProjectionMap.put(Imps.Contacts.CLIENT_TYPE,
    816                 "presence.client_type AS client_type");
    817 
    818         // Chats columns
    819         sContactsProjectionMap.put(Imps.Contacts.CHATS_CONTACT,
    820                 "chats.contact_id AS chats_contact_id");
    821         sContactsProjectionMap.put(Imps.Chats.JID_RESOURCE,
    822                 "chats.jid_resource AS jid_resource");
    823         sContactsProjectionMap.put(Imps.Chats.GROUP_CHAT,
    824                 "chats.groupchat AS groupchat");
    825         sContactsProjectionMap.put(Imps.Contacts.LAST_UNREAD_MESSAGE,
    826                 "chats.last_unread_message AS last_unread_message");
    827         sContactsProjectionMap.put(Imps.Contacts.LAST_MESSAGE_DATE,
    828                 "chats.last_message_date AS last_message_date");
    829         sContactsProjectionMap.put(Imps.Contacts.UNSENT_COMPOSED_MESSAGE,
    830                 "chats.unsent_composed_message AS unsent_composed_message");
    831         sContactsProjectionMap.put(Imps.Contacts.SHORTCUT, "chats.SHORTCUT AS shortcut");
    832 
    833         // Avatars columns
    834         sContactsProjectionMap.put(Imps.Contacts.AVATAR_HASH, "avatars.hash AS avatars_hash");
    835         sContactsProjectionMap.put(Imps.Contacts.AVATAR_DATA, "avatars.data AS avatars_data");
    836 
    837         // contactList projection map
    838         sContactListProjectionMap = new HashMap<String, String>();
    839         sContactListProjectionMap.put(Imps.ContactList._ID, "contactList._id AS _id");
    840         sContactListProjectionMap.put(Imps.ContactList._COUNT, "COUNT(*) AS _count");
    841         sContactListProjectionMap.put(Imps.ContactList.NAME, "name");
    842         sContactListProjectionMap.put(Imps.ContactList.PROVIDER, "provider");
    843         sContactListProjectionMap.put(Imps.ContactList.ACCOUNT, "account");
    844 
    845         // blockedList projection map
    846         sBlockedListProjectionMap = new HashMap<String, String>();
    847         sBlockedListProjectionMap.put(Imps.BlockedList._ID, "blockedList._id AS _id");
    848         sBlockedListProjectionMap.put(Imps.BlockedList._COUNT, "COUNT(*) AS _count");
    849         sBlockedListProjectionMap.put(Imps.BlockedList.USERNAME, "username");
    850         sBlockedListProjectionMap.put(Imps.BlockedList.NICKNAME, "nickname");
    851         sBlockedListProjectionMap.put(Imps.BlockedList.PROVIDER, "provider");
    852         sBlockedListProjectionMap.put(Imps.BlockedList.ACCOUNT, "account");
    853         sBlockedListProjectionMap.put(Imps.BlockedList.AVATAR_DATA,
    854                 "avatars.data AS avatars_data");
    855 
    856         // messages projection map
    857         sMessagesProjectionMap = new HashMap<String, String>();
    858         sMessagesProjectionMap.put(Imps.Messages._ID, "messages._id AS _id");
    859         sMessagesProjectionMap.put(Imps.Messages._COUNT, "COUNT(*) AS _count");
    860         sMessagesProjectionMap.put(Imps.Messages.THREAD_ID, "messages.thread_id AS thread_id");
    861         sMessagesProjectionMap.put(Imps.Messages.PACKET_ID, "messages.packet_id AS packet_id");
    862         sMessagesProjectionMap.put(Imps.Messages.NICKNAME, "messages.nickname AS nickname");
    863         sMessagesProjectionMap.put(Imps.Messages.BODY, "messages.body AS body");
    864         sMessagesProjectionMap.put(Imps.Messages.DATE, "messages.date AS date");
    865         sMessagesProjectionMap.put(Imps.Messages.TYPE, "messages.type AS type");
    866         sMessagesProjectionMap.put(Imps.Messages.ERROR_CODE, "messages.err_code AS err_code");
    867         sMessagesProjectionMap.put(Imps.Messages.ERROR_MESSAGE, "messages.err_msg AS err_msg");
    868         sMessagesProjectionMap.put(Imps.Messages.IS_GROUP_CHAT, "messages.is_muc AS is_muc");
    869         sMessagesProjectionMap.put(Imps.Messages.DISPLAY_SENT_TIME, "messages.show_ts AS show_ts");
    870         // contacts columns
    871         sMessagesProjectionMap.put(Imps.Messages.CONTACT, "contacts.username AS contact");
    872         sMessagesProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider AS provider");
    873         sMessagesProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account AS account");
    874         sMessagesProjectionMap.put("contact_type", "contacts.type AS contact_type");
    875 
    876         sInMemoryMessagesProjectionMap = new HashMap<String, String>();
    877         sInMemoryMessagesProjectionMap.put(Imps.Messages._ID,
    878                 "inMemoryMessages._id AS _id");
    879         sInMemoryMessagesProjectionMap.put(Imps.Messages._COUNT,
    880                 "COUNT(*) AS _count");
    881         sInMemoryMessagesProjectionMap.put(Imps.Messages.THREAD_ID,
    882                 "inMemoryMessages.thread_id AS thread_id");
    883         sInMemoryMessagesProjectionMap.put(Imps.Messages.PACKET_ID,
    884                 "inMemoryMessages.packet_id AS packet_id");
    885         sInMemoryMessagesProjectionMap.put(Imps.Messages.NICKNAME,
    886                 "inMemoryMessages.nickname AS nickname");
    887         sInMemoryMessagesProjectionMap.put(Imps.Messages.BODY,
    888                 "inMemoryMessages.body AS body");
    889         sInMemoryMessagesProjectionMap.put(Imps.Messages.DATE,
    890                 "inMemoryMessages.date AS date");
    891         sInMemoryMessagesProjectionMap.put(Imps.Messages.TYPE,
    892                 "inMemoryMessages.type AS type");
    893         sInMemoryMessagesProjectionMap.put(Imps.Messages.ERROR_CODE,
    894                 "inMemoryMessages.err_code AS err_code");
    895         sInMemoryMessagesProjectionMap.put(Imps.Messages.ERROR_MESSAGE,
    896                 "inMemoryMessages.err_msg AS err_msg");
    897         sInMemoryMessagesProjectionMap.put(Imps.Messages.IS_GROUP_CHAT,
    898                 "inMemoryMessages.is_muc AS is_muc");
    899         sInMemoryMessagesProjectionMap.put(Imps.Messages.DISPLAY_SENT_TIME,
    900                 "inMemoryMessages.show_ts AS show_ts");
    901         // contacts columns
    902         sInMemoryMessagesProjectionMap.put(Imps.Messages.CONTACT, "contacts.username AS contact");
    903         sInMemoryMessagesProjectionMap.put(Imps.Contacts.PROVIDER, "contacts.provider AS provider");
    904         sInMemoryMessagesProjectionMap.put(Imps.Contacts.ACCOUNT, "contacts.account AS account");
    905         sInMemoryMessagesProjectionMap.put("contact_type", "contacts.type AS contact_type");
    906     }
    907 
    908     public ImpsProvider() {
    909         this(DATABASE_NAME, DATABASE_VERSION);
    910 
    911         setupImUrlMatchers(AUTHORITY);
    912         setupMcsUrlMatchers(AUTHORITY);
    913     }
    914 
    915     protected ImpsProvider(String dbName, int dbVersion) {
    916         mDatabaseName = dbName;
    917         mDatabaseVersion = dbVersion;
    918         mTransientDbName = "transient_" + dbName.replace(".", "_");
    919     }
    920 
    921     private void setupImUrlMatchers(String authority) {
    922         mUrlMatcher.addURI(authority, "providers", MATCH_PROVIDERS);
    923         mUrlMatcher.addURI(authority, "providers/#", MATCH_PROVIDERS_BY_ID);
    924         mUrlMatcher.addURI(authority, "providers/account", MATCH_PROVIDERS_WITH_ACCOUNT);
    925 
    926         mUrlMatcher.addURI(authority, "accounts", MATCH_ACCOUNTS);
    927         mUrlMatcher.addURI(authority, "accounts/#", MATCH_ACCOUNTS_BY_ID);
    928 
    929         mUrlMatcher.addURI(authority, "contacts", MATCH_CONTACTS);
    930         mUrlMatcher.addURI(authority, "contactsWithPresence", MATCH_CONTACTS_JOIN_PRESENCE);
    931         mUrlMatcher.addURI(authority, "contactsBarebone", MATCH_CONTACTS_BAREBONE);
    932         mUrlMatcher.addURI(authority, "contacts/#/#", MATCH_CONTACTS_BY_PROVIDER);
    933         mUrlMatcher.addURI(authority, "contacts/chatting", MATCH_CHATTING_CONTACTS);
    934         mUrlMatcher.addURI(authority, "contacts/chatting/#/#", MATCH_CHATTING_CONTACTS_BY_PROVIDER);
    935         mUrlMatcher.addURI(authority, "contacts/online/#/#", MATCH_ONLINE_CONTACTS_BY_PROVIDER);
    936         mUrlMatcher.addURI(authority, "contacts/offline/#/#", MATCH_OFFLINE_CONTACTS_BY_PROVIDER);
    937         mUrlMatcher.addURI(authority, "contacts/#", MATCH_CONTACT);
    938         mUrlMatcher.addURI(authority, "contacts/blocked", MATCH_BLOCKED_CONTACTS);
    939         mUrlMatcher.addURI(authority, "bulk_contacts", MATCH_CONTACTS_BULK);
    940         mUrlMatcher.addURI(authority, "contacts/onlineCount", MATCH_ONLINE_CONTACT_COUNT);
    941 
    942         mUrlMatcher.addURI(authority, "contactLists", MATCH_CONTACTLISTS);
    943         mUrlMatcher.addURI(authority, "contactLists/#/#", MATCH_CONTACTLISTS_BY_PROVIDER);
    944         mUrlMatcher.addURI(authority, "contactLists/#", MATCH_CONTACTLIST);
    945         mUrlMatcher.addURI(authority, "blockedList", MATCH_BLOCKEDLIST);
    946         mUrlMatcher.addURI(authority, "blockedList/#/#", MATCH_BLOCKEDLIST_BY_PROVIDER);
    947 
    948         mUrlMatcher.addURI(authority, "contactsEtag", MATCH_CONTACTS_ETAGS);
    949         mUrlMatcher.addURI(authority, "contactsEtag/#", MATCH_CONTACTS_ETAG);
    950 
    951         mUrlMatcher.addURI(authority, "presence", MATCH_PRESENCE);
    952         mUrlMatcher.addURI(authority, "presence/#", MATCH_PRESENCE_ID);
    953         mUrlMatcher.addURI(authority, "presence/account/#", MATCH_PRESENCE_BY_ACCOUNT);
    954         mUrlMatcher.addURI(authority, "seed_presence/account/#", MATCH_PRESENCE_SEED_BY_ACCOUNT);
    955         mUrlMatcher.addURI(authority, "bulk_presence", MATCH_PRESENCE_BULK);
    956 
    957         mUrlMatcher.addURI(authority, "messages", MATCH_MESSAGES);
    958         mUrlMatcher.addURI(authority, "messagesByAcctAndContact/#/*", MATCH_MESSAGES_BY_CONTACT);
    959         mUrlMatcher.addURI(authority, "messagesByThreadId/#", MATCH_MESSAGES_BY_THREAD_ID);
    960         mUrlMatcher.addURI(authority, "messagesByProvider/#", MATCH_MESSAGES_BY_PROVIDER);
    961         mUrlMatcher.addURI(authority, "messagesByAccount/#", MATCH_MESSAGES_BY_ACCOUNT);
    962         mUrlMatcher.addURI(authority, "messages/#", MATCH_MESSAGE);
    963 
    964         mUrlMatcher.addURI(authority, "otrMessages", MATCH_OTR_MESSAGES);
    965         mUrlMatcher.addURI(authority, "otrMessagesByAcctAndContact/#/*",
    966                 MATCH_OTR_MESSAGES_BY_CONTACT);
    967         mUrlMatcher.addURI(authority, "otrMessagesByThreadId/#", MATCH_OTR_MESSAGES_BY_THREAD_ID);
    968         mUrlMatcher.addURI(authority, "otrMessagesByProvider/#", MATCH_OTR_MESSAGES_BY_PROVIDER);
    969         mUrlMatcher.addURI(authority, "otrMessagesByAccount/#", MATCH_OTR_MESSAGES_BY_ACCOUNT);
    970         mUrlMatcher.addURI(authority, "otrMessages/#", MATCH_OTR_MESSAGE);
    971 
    972         mUrlMatcher.addURI(authority, "groupMembers", MATCH_GROUP_MEMBERS);
    973         mUrlMatcher.addURI(authority, "groupMembers/#", MATCH_GROUP_MEMBERS_BY_GROUP);
    974 
    975         mUrlMatcher.addURI(authority, "avatars", MATCH_AVATARS);
    976         mUrlMatcher.addURI(authority, "avatars/#", MATCH_AVATAR);
    977         mUrlMatcher.addURI(authority, "avatarsBy/#/#", MATCH_AVATAR_BY_PROVIDER);
    978         mUrlMatcher.addURI(authority, "chats", MATCH_CHATS);
    979         mUrlMatcher.addURI(authority, "chats/account/#", MATCH_CHATS_BY_ACCOUNT);
    980         mUrlMatcher.addURI(authority, "chats/#", MATCH_CHATS_ID);
    981 
    982         mUrlMatcher.addURI(authority, "sessionCookies", MATCH_SESSIONS);
    983         mUrlMatcher.addURI(authority, "sessionCookiesBy/#/#", MATCH_SESSIONS_BY_PROVIDER);
    984         mUrlMatcher.addURI(authority, "providerSettings", MATCH_PROVIDER_SETTINGS);
    985         mUrlMatcher.addURI(authority, "providerSettings/#", MATCH_PROVIDER_SETTINGS_BY_ID);
    986         mUrlMatcher.addURI(authority, "providerSettings/#/*",
    987                 MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME);
    988 
    989         mUrlMatcher.addURI(authority, "invitations", MATCH_INVITATIONS);
    990         mUrlMatcher.addURI(authority, "invitations/#", MATCH_INVITATION);
    991 
    992         mUrlMatcher.addURI(authority, "accountStatus", MATCH_ACCOUNTS_STATUS);
    993         mUrlMatcher.addURI(authority, "accountStatus/#", MATCH_ACCOUNT_STATUS);
    994 
    995         mUrlMatcher.addURI(authority, "brandingResMapCache", MATCH_BRANDING_RESOURCE_MAP_CACHE);
    996     }
    997 
    998     private void setupMcsUrlMatchers(String authority) {
    999         mUrlMatcher.addURI(authority, "outgoingRmqMessages", MATCH_OUTGOING_RMQ_MESSAGES);
   1000         mUrlMatcher.addURI(authority, "outgoingRmqMessages/#", MATCH_OUTGOING_RMQ_MESSAGE);
   1001         mUrlMatcher.addURI(authority, "outgoingHighestRmqId", MATCH_OUTGOING_HIGHEST_RMQ_ID);
   1002         mUrlMatcher.addURI(authority, "lastRmqId", MATCH_LAST_RMQ_ID);
   1003         mUrlMatcher.addURI(authority, "s2dids", MATCH_S2D_RMQ_IDS);
   1004     }
   1005 
   1006     @Override
   1007     public boolean onCreate() {
   1008         mOpenHelper = new DatabaseHelper(getContext());
   1009         return true;
   1010     }
   1011 
   1012     @Override
   1013     public final int update(final Uri url, final ContentValues values,
   1014             final String selection, final String[] selectionArgs) {
   1015 
   1016         int result = 0;
   1017         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1018         db.beginTransaction();
   1019         try {
   1020             result = updateInternal(url, values, selection, selectionArgs);
   1021             db.setTransactionSuccessful();
   1022         } finally {
   1023             db.endTransaction();
   1024         }
   1025         if (result > 0) {
   1026             getContext().getContentResolver()
   1027                     .notifyChange(url, null /* observer */, false /* sync */);
   1028         }
   1029         return result;
   1030     }
   1031 
   1032     @Override
   1033     public final int delete(final Uri url, final String selection,
   1034             final String[] selectionArgs) {
   1035         int result;
   1036         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1037         db.beginTransaction();
   1038         try {
   1039             result = deleteInternal(url, selection, selectionArgs);
   1040             db.setTransactionSuccessful();
   1041         } finally {
   1042             db.endTransaction();
   1043         }
   1044         if (result > 0) {
   1045             getContext().getContentResolver()
   1046                     .notifyChange(url, null /* observer */, false /* sync */);
   1047         }
   1048         return result;
   1049     }
   1050 
   1051     @Override
   1052     public final Uri insert(final Uri url, final ContentValues values) {
   1053         Uri result;
   1054         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1055         db.beginTransaction();
   1056         try {
   1057             result = insertInternal(url, values);
   1058             db.setTransactionSuccessful();
   1059         } finally {
   1060             db.endTransaction();
   1061         }
   1062         if (result != null) {
   1063             getContext().getContentResolver()
   1064                     .notifyChange(url, null /* observer */, false /* sync */);
   1065         }
   1066         return result;
   1067     }
   1068 
   1069     @Override
   1070     public final Cursor query(final Uri url, final String[] projection,
   1071             final String selection, final String[] selectionArgs,
   1072             final String sortOrder) {
   1073         return queryInternal(url, projection, selection, selectionArgs, sortOrder);
   1074     }
   1075 
   1076     public Cursor queryInternal(Uri url, String[] projectionIn,
   1077             String selection, String[] selectionArgs, String sort) {
   1078         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   1079         StringBuilder whereClause = new StringBuilder();
   1080         if(selection != null) {
   1081             whereClause.append(selection);
   1082         }
   1083         String groupBy = null;
   1084         String limit = null;
   1085 
   1086         // Generate the body of the query
   1087         int match = mUrlMatcher.match(url);
   1088 
   1089         if (DBG) {
   1090             log("query " + url + ", match " + match + ", where " + selection);
   1091             if (selectionArgs != null) {
   1092                 for (String selectionArg : selectionArgs) {
   1093                     log("     selectionArg: " + selectionArg);
   1094                 }
   1095             }
   1096         }
   1097 
   1098         switch (match) {
   1099             case MATCH_PROVIDERS_BY_ID:
   1100                 appendWhere(whereClause, Imps.Provider._ID, "=", url.getPathSegments().get(1));
   1101                 // fall thru.
   1102 
   1103             case MATCH_PROVIDERS:
   1104                 qb.setTables(TABLE_PROVIDERS);
   1105                 break;
   1106 
   1107             case MATCH_PROVIDERS_WITH_ACCOUNT:
   1108                 qb.setTables(PROVIDER_JOIN_ACCOUNT_TABLE);
   1109                 qb.setProjectionMap(sProviderAccountsProjectionMap);
   1110                 break;
   1111 
   1112             case MATCH_ACCOUNTS_BY_ID:
   1113                 appendWhere(whereClause, Imps.Account._ID, "=", url.getPathSegments().get(1));
   1114                 // falls down
   1115             case MATCH_ACCOUNTS:
   1116                 qb.setTables(TABLE_ACCOUNTS);
   1117                 break;
   1118 
   1119             case MATCH_CONTACTS:
   1120                 qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
   1121                 qb.setProjectionMap(sContactsProjectionMap);
   1122                 break;
   1123 
   1124             case MATCH_CONTACTS_JOIN_PRESENCE:
   1125                 qb.setTables(CONTACT_JOIN_PRESENCE_TABLE);
   1126                 qb.setProjectionMap(sContactsProjectionMap);
   1127                 break;
   1128 
   1129             case MATCH_CONTACTS_BAREBONE:
   1130                 qb.setTables(TABLE_CONTACTS);
   1131                 break;
   1132 
   1133             case MATCH_CHATTING_CONTACTS:
   1134                 qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
   1135                 qb.setProjectionMap(sContactsProjectionMap);
   1136                 appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
   1137                 // no need to add the non blocked contacts clause because
   1138                 // blocked contacts can't have conversations.
   1139                 break;
   1140 
   1141             case MATCH_CONTACTS_BY_PROVIDER:
   1142                 buildQueryContactsByProvider(qb, whereClause, url);
   1143                 appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
   1144                 break;
   1145 
   1146             case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
   1147                 buildQueryContactsByProvider(qb, whereClause, url);
   1148                 appendWhere(whereClause, "chats.last_message_date IS NOT NULL");
   1149                 // no need to add the non blocked contacts clause because
   1150                 // blocked contacts can't have conversations.
   1151                 break;
   1152 
   1153             case MATCH_NO_CHATTING_CONTACTS_BY_PROVIDER:
   1154                 buildQueryContactsByProvider(qb, whereClause, url);
   1155                 appendWhere(whereClause, "chats.last_message_date IS NULL");
   1156                 appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
   1157                 break;
   1158 
   1159             case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
   1160                 buildQueryContactsByProvider(qb, whereClause, url);
   1161                 appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "!=", Imps.Presence.OFFLINE);
   1162                 appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
   1163                 break;
   1164 
   1165             case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
   1166                 buildQueryContactsByProvider(qb, whereClause, url);
   1167                 appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "=", Imps.Presence.OFFLINE);
   1168                 appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
   1169                 break;
   1170 
   1171             case MATCH_BLOCKED_CONTACTS:
   1172                 qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
   1173                 qb.setProjectionMap(sContactsProjectionMap);
   1174                 appendWhere(whereClause, BLOCKED_CONTACTS_WHERE_CLAUSE);
   1175                 break;
   1176 
   1177             case MATCH_CONTACT:
   1178                 qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
   1179                 qb.setProjectionMap(sContactsProjectionMap);
   1180                 appendWhere(whereClause, "contacts._id", "=", url.getPathSegments().get(1));
   1181                 break;
   1182 
   1183             case MATCH_ONLINE_CONTACT_COUNT:
   1184                 qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_TABLE);
   1185                 qb.setProjectionMap(sContactsProjectionMap);
   1186                 appendWhere(whereClause, Imps.Contacts.PRESENCE_STATUS, "!=", Imps.Presence.OFFLINE);
   1187                 appendWhere(whereClause, "chats.last_message_date IS NULL");
   1188                 appendWhere(whereClause, NON_BLOCKED_CONTACTS_WHERE_CLAUSE);
   1189                 groupBy = Imps.Contacts.CONTACTLIST;
   1190                 break;
   1191 
   1192             case MATCH_CONTACTLISTS_BY_PROVIDER:
   1193                 appendWhere(whereClause, Imps.ContactList.ACCOUNT, "=",
   1194                         url.getPathSegments().get(2));
   1195                 // fall through
   1196             case MATCH_CONTACTLISTS:
   1197                 qb.setTables(TABLE_CONTACT_LIST);
   1198                 qb.setProjectionMap(sContactListProjectionMap);
   1199                 break;
   1200 
   1201             case MATCH_CONTACTLIST:
   1202                 qb.setTables(TABLE_CONTACT_LIST);
   1203                 appendWhere(whereClause, Imps.ContactList._ID, "=", url.getPathSegments().get(1));
   1204                 break;
   1205 
   1206             case MATCH_BLOCKEDLIST:
   1207                 qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
   1208                 qb.setProjectionMap(sBlockedListProjectionMap);
   1209                 break;
   1210 
   1211             case MATCH_BLOCKEDLIST_BY_PROVIDER:
   1212                 qb.setTables(BLOCKEDLIST_JOIN_AVATAR_TABLE);
   1213                 qb.setProjectionMap(sBlockedListProjectionMap);
   1214                 appendWhere(whereClause, Imps.BlockedList.ACCOUNT, "=",
   1215                         url.getPathSegments().get(2));
   1216                 break;
   1217 
   1218             case MATCH_CONTACTS_ETAGS:
   1219                 qb.setTables(TABLE_CONTACTS_ETAG);
   1220                 break;
   1221 
   1222             case MATCH_CONTACTS_ETAG:
   1223                 qb.setTables(TABLE_CONTACTS_ETAG);
   1224                 appendWhere(whereClause, "_id", "=", url.getPathSegments().get(1));
   1225                 break;
   1226 
   1227             case MATCH_MESSAGES_BY_THREAD_ID:
   1228                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", url.getPathSegments().get(1));
   1229                 // fall thru.
   1230 
   1231             case MATCH_MESSAGES:
   1232                 qb.setTables(TABLE_MESSAGES);
   1233 
   1234                 final String selectionClause = whereClause.toString();
   1235                 final String query1 = qb.buildQuery(projectionIn, selectionClause,
   1236                         null, null, null, null, null /* limit */);
   1237 
   1238                 // Build the second query for frequent
   1239                 qb = new SQLiteQueryBuilder();
   1240                 qb.setTables(TABLE_IN_MEMORY_MESSAGES);
   1241                 final String query2 = qb.buildQuery(projectionIn,
   1242                         selectionClause, null, null, null, null, null /* limit */);
   1243 
   1244                 // Put them together
   1245                 final String query = qb.buildUnionQuery(new String[] {query1, query2}, sort, null);
   1246                 final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1247                 Cursor c = db.rawQueryWithFactory(null, query, null, TABLE_MESSAGES);
   1248                 if ((c != null) && !isTemporary()) {
   1249                     c.setNotificationUri(getContext().getContentResolver(), url);
   1250                 }
   1251                 return c;
   1252 
   1253             case MATCH_MESSAGE:
   1254                 qb.setTables(TABLE_MESSAGES);
   1255                 appendWhere(whereClause, Imps.Messages._ID, "=", url.getPathSegments().get(1));
   1256                 break;
   1257 
   1258             case MATCH_MESSAGES_BY_CONTACT:
   1259                 qb.setTables(MESSAGE_JOIN_CONTACT_TABLE);
   1260                 qb.setProjectionMap(sMessagesProjectionMap);
   1261 
   1262                 appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getPathSegments().get(1));
   1263                 appendWhere(whereClause, "contacts.username", "=",
   1264                         decodeURLSegment(url.getPathSegments().get(2)));
   1265 
   1266                 final String sel = whereClause.toString();
   1267                 final String q1 = qb.buildQuery(projectionIn, sel, null, null, null, null, null);
   1268 
   1269                 // Build the second query for frequent
   1270                 qb = new SQLiteQueryBuilder();
   1271                 qb.setTables(IN_MEMORY_MESSAGES_JOIN_CONTACT_TABLE);
   1272                 qb.setProjectionMap(sInMemoryMessagesProjectionMap);
   1273                 final String q2 = qb.buildQuery(projectionIn, sel, null, null, null, null, null);
   1274 
   1275                 // Put them together
   1276                 final String q3 = qb.buildUnionQuery(new String[] {q1, q2}, sort, null);
   1277                 final SQLiteDatabase db2 = mOpenHelper.getWritableDatabase();
   1278                 Cursor c2 = db2.rawQueryWithFactory(null, q3, null, MESSAGE_JOIN_CONTACT_TABLE);
   1279                 if ((c2 != null) && !isTemporary()) {
   1280                     c2.setNotificationUri(getContext().getContentResolver(), url);
   1281                 }
   1282                 return c2;
   1283 
   1284             case MATCH_INVITATIONS:
   1285                 qb.setTables(TABLE_INVITATIONS);
   1286                 break;
   1287 
   1288             case MATCH_INVITATION:
   1289                 qb.setTables(TABLE_INVITATIONS);
   1290                 appendWhere(whereClause, Imps.Invitation._ID, "=", url.getPathSegments().get(1));
   1291                 break;
   1292 
   1293             case MATCH_GROUP_MEMBERS:
   1294                 qb.setTables(TABLE_GROUP_MEMBERS);
   1295                 break;
   1296 
   1297             case MATCH_GROUP_MEMBERS_BY_GROUP:
   1298                 qb.setTables(TABLE_GROUP_MEMBERS);
   1299                 appendWhere(whereClause, Imps.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
   1300                 break;
   1301 
   1302             case MATCH_AVATARS:
   1303                 qb.setTables(TABLE_AVATARS);
   1304                 break;
   1305 
   1306             case MATCH_AVATAR_BY_PROVIDER:
   1307                 qb.setTables(TABLE_AVATARS);
   1308                 appendWhere(whereClause, Imps.Avatars.ACCOUNT, "=", url.getPathSegments().get(2));
   1309                 break;
   1310 
   1311             case MATCH_CHATS:
   1312                 qb.setTables(TABLE_CHATS);
   1313                 break;
   1314 
   1315             case MATCH_CHATS_ID:
   1316                 qb.setTables(TABLE_CHATS);
   1317                 appendWhere(whereClause, Imps.Chats.CONTACT_ID, "=", url.getPathSegments().get(1));
   1318                 break;
   1319 
   1320             case MATCH_CHATS_BY_ACCOUNT:
   1321                 qb.setTables(TABLE_CHATS);
   1322                 String accountStr = decodeURLSegment(url.getLastPathSegment());
   1323                 appendWhere(whereClause, buildContactIdSelection(Imps.Chats.CONTACT_ID,
   1324                         Imps.Contacts.ACCOUNT + "='" + accountStr + "'"));
   1325                 break;
   1326 
   1327             case MATCH_PRESENCE:
   1328                 qb.setTables(TABLE_PRESENCE);
   1329                 break;
   1330 
   1331             case MATCH_PRESENCE_ID:
   1332                 qb.setTables(TABLE_PRESENCE);
   1333                 appendWhere(whereClause, Imps.Presence.CONTACT_ID, "=", url.getPathSegments().get(1));
   1334                 break;
   1335 
   1336             case MATCH_SESSIONS:
   1337                 qb.setTables(TABLE_SESSION_COOKIES);
   1338                 break;
   1339 
   1340             case MATCH_SESSIONS_BY_PROVIDER:
   1341                 qb.setTables(TABLE_SESSION_COOKIES);
   1342                 appendWhere(whereClause, Imps.SessionCookies.ACCOUNT, "=", url.getPathSegments().get(2));
   1343                 break;
   1344 
   1345             case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
   1346                 appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", url.getPathSegments().get(2));
   1347                 // fall through
   1348             case MATCH_PROVIDER_SETTINGS_BY_ID:
   1349                 appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", url.getPathSegments().get(1));
   1350                 // fall through
   1351             case MATCH_PROVIDER_SETTINGS:
   1352                 qb.setTables(TABLE_PROVIDER_SETTINGS);
   1353                 break;
   1354 
   1355             case MATCH_ACCOUNTS_STATUS:
   1356                 qb.setTables(TABLE_ACCOUNT_STATUS);
   1357                 break;
   1358 
   1359             case MATCH_ACCOUNT_STATUS:
   1360                 qb.setTables(TABLE_ACCOUNT_STATUS);
   1361                 appendWhere(whereClause, Imps.AccountStatus.ACCOUNT, "=",
   1362                         url.getPathSegments().get(1));
   1363                 break;
   1364 
   1365             case MATCH_BRANDING_RESOURCE_MAP_CACHE:
   1366                 qb.setTables(TABLE_BRANDING_RESOURCE_MAP_CACHE);
   1367                 break;
   1368 
   1369             // mcs and rmq queries
   1370             case MATCH_OUTGOING_RMQ_MESSAGES:
   1371                 qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
   1372                 break;
   1373 
   1374             case MATCH_OUTGOING_HIGHEST_RMQ_ID:
   1375                 qb.setTables(TABLE_OUTGOING_RMQ_MESSAGES);
   1376                 sort = "rmq_id DESC";
   1377                 limit = "1";
   1378                 break;
   1379 
   1380             case MATCH_LAST_RMQ_ID:
   1381                 qb.setTables(TABLE_LAST_RMQ_ID);
   1382                 limit = "1";
   1383                 break;
   1384 
   1385             case MATCH_S2D_RMQ_IDS:
   1386                 qb.setTables(TABLE_S2D_RMQ_IDS);
   1387                 break;
   1388 
   1389             default:
   1390                 throw new IllegalArgumentException("Unknown URL " + url);
   1391         }
   1392 
   1393         // run the query
   1394         final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   1395         Cursor c = null;
   1396 
   1397         try {
   1398             c = qb.query(db, projectionIn, whereClause.toString(), selectionArgs,
   1399                     groupBy, null, sort, limit);
   1400             if (c != null) {
   1401                 switch(match) {
   1402                 case MATCH_CHATTING_CONTACTS:
   1403                 case MATCH_CONTACTS_BY_PROVIDER:
   1404                 case MATCH_CHATTING_CONTACTS_BY_PROVIDER:
   1405                 case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
   1406                 case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
   1407                 case MATCH_CONTACTS_BAREBONE:
   1408                 case MATCH_CONTACTS_JOIN_PRESENCE:
   1409                 case MATCH_ONLINE_CONTACT_COUNT:
   1410                     url = Imps.Contacts.CONTENT_URI;
   1411                     break;
   1412                 }
   1413                 if (DBG) log("set notify url " + url);
   1414                 c.setNotificationUri(getContext().getContentResolver(), url);
   1415             }
   1416         } catch (Exception ex) {
   1417             Log.e(LOG_TAG, "query db caught ", ex);
   1418         }
   1419 
   1420         return c;
   1421     }
   1422 
   1423     private void buildQueryContactsByProvider(SQLiteQueryBuilder qb,
   1424             StringBuilder whereClause, Uri url) {
   1425         qb.setTables(CONTACT_JOIN_PRESENCE_CHAT_AVATAR_TABLE);
   1426         qb.setProjectionMap(sContactsProjectionMap);
   1427         // we don't really need the provider id in query. account id is enough.
   1428         appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getLastPathSegment());
   1429     }
   1430 
   1431     @Override
   1432     public String getType(Uri url) {
   1433         int match = mUrlMatcher.match(url);
   1434         switch (match) {
   1435             case MATCH_PROVIDERS:
   1436                 return Imps.Provider.CONTENT_TYPE;
   1437 
   1438             case MATCH_PROVIDERS_BY_ID:
   1439                 return Imps.Provider.CONTENT_ITEM_TYPE;
   1440 
   1441             case MATCH_ACCOUNTS:
   1442                 return Imps.Account.CONTENT_TYPE;
   1443 
   1444             case MATCH_ACCOUNTS_BY_ID:
   1445                 return Imps.Account.CONTENT_ITEM_TYPE;
   1446 
   1447             case MATCH_CONTACTS:
   1448             case MATCH_CONTACTS_BY_PROVIDER:
   1449             case MATCH_ONLINE_CONTACTS_BY_PROVIDER:
   1450             case MATCH_OFFLINE_CONTACTS_BY_PROVIDER:
   1451             case MATCH_CONTACTS_BULK:
   1452             case MATCH_CONTACTS_BAREBONE:
   1453             case MATCH_CONTACTS_JOIN_PRESENCE:
   1454                 return Imps.Contacts.CONTENT_TYPE;
   1455 
   1456             case MATCH_CONTACT:
   1457                 return Imps.Contacts.CONTENT_ITEM_TYPE;
   1458 
   1459             case MATCH_CONTACTLISTS:
   1460             case MATCH_CONTACTLISTS_BY_PROVIDER:
   1461                 return Imps.ContactList.CONTENT_TYPE;
   1462 
   1463             case MATCH_CONTACTLIST:
   1464                 return Imps.ContactList.CONTENT_ITEM_TYPE;
   1465 
   1466             case MATCH_BLOCKEDLIST:
   1467             case MATCH_BLOCKEDLIST_BY_PROVIDER:
   1468                 return Imps.BlockedList.CONTENT_TYPE;
   1469 
   1470             case MATCH_CONTACTS_ETAGS:
   1471             case MATCH_CONTACTS_ETAG:
   1472                 return Imps.ContactsEtag.CONTENT_TYPE;
   1473 
   1474             case MATCH_MESSAGES:
   1475             case MATCH_MESSAGES_BY_CONTACT:
   1476             case MATCH_MESSAGES_BY_THREAD_ID:
   1477             case MATCH_MESSAGES_BY_PROVIDER:
   1478             case MATCH_MESSAGES_BY_ACCOUNT:
   1479             case MATCH_OTR_MESSAGES:
   1480             case MATCH_OTR_MESSAGES_BY_CONTACT:
   1481             case MATCH_OTR_MESSAGES_BY_THREAD_ID:
   1482             case MATCH_OTR_MESSAGES_BY_PROVIDER:
   1483             case MATCH_OTR_MESSAGES_BY_ACCOUNT:
   1484                 return Imps.Messages.CONTENT_TYPE;
   1485 
   1486             case MATCH_MESSAGE:
   1487             case MATCH_OTR_MESSAGE:
   1488                 return Imps.Messages.CONTENT_ITEM_TYPE;
   1489 
   1490             case MATCH_PRESENCE:
   1491             case MATCH_PRESENCE_BULK:
   1492                 return Imps.Presence.CONTENT_TYPE;
   1493 
   1494             case MATCH_AVATARS:
   1495                 return Imps.Avatars.CONTENT_TYPE;
   1496 
   1497             case MATCH_AVATAR:
   1498                 return Imps.Avatars.CONTENT_ITEM_TYPE;
   1499 
   1500             case MATCH_CHATS:
   1501                 return Imps.Chats.CONTENT_TYPE;
   1502 
   1503             case MATCH_CHATS_ID:
   1504                 return Imps.Chats.CONTENT_ITEM_TYPE;
   1505 
   1506             case MATCH_INVITATIONS:
   1507                 return Imps.Invitation.CONTENT_TYPE;
   1508 
   1509             case MATCH_INVITATION:
   1510                 return Imps.Invitation.CONTENT_ITEM_TYPE;
   1511 
   1512             case MATCH_GROUP_MEMBERS:
   1513             case MATCH_GROUP_MEMBERS_BY_GROUP:
   1514                 return Imps.GroupMembers.CONTENT_TYPE;
   1515 
   1516             case MATCH_SESSIONS:
   1517             case MATCH_SESSIONS_BY_PROVIDER:
   1518                 return Imps.SessionCookies.CONTENT_TYPE;
   1519 
   1520             case MATCH_PROVIDER_SETTINGS:
   1521                 return Imps.ProviderSettings.CONTENT_TYPE;
   1522 
   1523             case MATCH_ACCOUNTS_STATUS:
   1524                 return Imps.AccountStatus.CONTENT_TYPE;
   1525 
   1526             case MATCH_ACCOUNT_STATUS:
   1527                 return Imps.AccountStatus.CONTENT_ITEM_TYPE;
   1528 
   1529             default:
   1530                 throw new IllegalArgumentException("Unknown URL");
   1531         }
   1532     }
   1533 
   1534     // package scope for testing.
   1535     boolean insertBulkContacts(ContentValues values) {
   1536         //if (DBG) log("insertBulkContacts: begin");
   1537 
   1538         ArrayList<String> usernames = values.getStringArrayList(Imps.Contacts.USERNAME);
   1539         ArrayList<String> nicknames = values.getStringArrayList(Imps.Contacts.NICKNAME);
   1540         int usernameCount = usernames.size();
   1541         int nicknameCount = nicknames.size();
   1542 
   1543         if (usernameCount != nicknameCount) {
   1544             Log.e(LOG_TAG, "[ImProvider] insertBulkContacts: input bundle " +
   1545                     "username & nickname lists have diff. length!");
   1546             return false;
   1547         }
   1548 
   1549         ArrayList<String> contactTypeArray = values.getStringArrayList(Imps.Contacts.TYPE);
   1550         ArrayList<String> subscriptionStatusArray =
   1551                 values.getStringArrayList(Imps.Contacts.SUBSCRIPTION_STATUS);
   1552         ArrayList<String> subscriptionTypeArray =
   1553                 values.getStringArrayList(Imps.Contacts.SUBSCRIPTION_TYPE);
   1554         ArrayList<String> quickContactArray = values.getStringArrayList(Imps.Contacts.QUICK_CONTACT);
   1555         ArrayList<String> rejectedArray = values.getStringArrayList(Imps.Contacts.REJECTED);
   1556         int sum = 0;
   1557 
   1558         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1559 
   1560         db.beginTransaction();
   1561         try {
   1562             Long provider = values.getAsLong(Imps.Contacts.PROVIDER);
   1563             Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
   1564             Long listId = values.getAsLong(Imps.Contacts.CONTACTLIST);
   1565 
   1566             ContentValues contactValues = new ContentValues();
   1567             contactValues.put(Imps.Contacts.PROVIDER, provider);
   1568             contactValues.put(Imps.Contacts.ACCOUNT, account);
   1569             contactValues.put(Imps.Contacts.CONTACTLIST, listId);
   1570             ContentValues presenceValues = new ContentValues();
   1571             presenceValues.put(Imps.Presence.PRESENCE_STATUS,
   1572                     Imps.Presence.OFFLINE);
   1573 
   1574             for (int i=0; i<usernameCount; i++) {
   1575                 String username = usernames.get(i);
   1576                 String nickname = nicknames.get(i);
   1577                 int type = 0;
   1578                 int subscriptionStatus = 0;
   1579                 int subscriptionType = 0;
   1580                 int quickContact = 0;
   1581                 int rejected = 0;
   1582 
   1583                 try {
   1584                     type = Integer.parseInt(contactTypeArray.get(i));
   1585                     if (subscriptionStatusArray != null) {
   1586                         subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
   1587                     }
   1588                     if (subscriptionTypeArray != null) {
   1589                         subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
   1590                     }
   1591                     if (quickContactArray != null) {
   1592                         quickContact = Integer.parseInt(quickContactArray.get(i));
   1593                     }
   1594                     if (rejectedArray != null) {
   1595                         rejected = Integer.parseInt(rejectedArray.get(i));
   1596                     }
   1597                 } catch (NumberFormatException ex) {
   1598                     Log.e(LOG_TAG, "insertBulkContacts: caught " + ex);
   1599                 }
   1600 
   1601                 /*
   1602                 if (DBG) log("insertBulkContacts[" + i + "] username=" +
   1603                         username + ", nickname=" + nickname + ", type=" + type +
   1604                         ", subscriptionStatus=" + subscriptionStatus + ", subscriptionType=" +
   1605                         subscriptionType + ", qc=" + quickContact);
   1606                 */
   1607 
   1608                 contactValues.put(Imps.Contacts.USERNAME, username);
   1609                 contactValues.put(Imps.Contacts.NICKNAME, nickname);
   1610                 contactValues.put(Imps.Contacts.TYPE, type);
   1611                 if (subscriptionStatusArray != null) {
   1612                     contactValues.put(Imps.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
   1613                 }
   1614                 if (subscriptionTypeArray != null) {
   1615                     contactValues.put(Imps.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
   1616                 }
   1617                 if (quickContactArray != null) {
   1618                     contactValues.put(Imps.Contacts.QUICK_CONTACT, quickContact);
   1619                 }
   1620                 if (rejectedArray != null) {
   1621                     contactValues.put(Imps.Contacts.REJECTED, rejected);
   1622                 }
   1623 
   1624                 long rowId;
   1625 
   1626                 /* save this code for when we add constraint (account, username) to the contacts
   1627                    table
   1628                 try {
   1629                     rowId = db.insertOrThrow(TABLE_CONTACTS, USERNAME, contactValues);
   1630                 } catch (android.database.sqlite.SQLiteConstraintException ex) {
   1631                     if (DBG) log("insertBulkContacts: insert " + username + " caught " + ex);
   1632 
   1633                     // append username to the selection clause
   1634                     updateSelection.delete(0, updateSelection.length());
   1635                     updateSelection.append(Im.Contacts.USERNAME);
   1636                     updateSelection.append("=?");
   1637                     updateSelectionArgs[0] = username;
   1638 
   1639                     int updated = db.update(TABLE_CONTACTS, contactValues,
   1640                             updateSelection.toString(), updateSelectionArgs);
   1641 
   1642                     if (DBG && updated != 1) {
   1643                         log("insertBulkContacts: update " + username + " failed!");
   1644                     }
   1645                 }
   1646                 */
   1647 
   1648                 rowId = db.insert(TABLE_CONTACTS, USERNAME, contactValues);
   1649                 if (rowId > 0) {
   1650                     sum++;
   1651 
   1652                     // seed the presence for the new contact
   1653                     if (DBG) log("### seedPresence for contact id " + rowId);
   1654                     presenceValues.put(Imps.Presence.CONTACT_ID, rowId);
   1655 
   1656                     try {
   1657                         db.insert(TABLE_PRESENCE, null, presenceValues);
   1658                     } catch (android.database.sqlite.SQLiteConstraintException ex) {
   1659                         Log.w(LOG_TAG, "insertBulkContacts: seeding presence caught " + ex);
   1660                     }
   1661                 }
   1662 
   1663                 // yield the lock if anyone else is trying to
   1664                 // perform a db operation here.
   1665                 db.yieldIfContended();
   1666             }
   1667 
   1668             db.setTransactionSuccessful();
   1669         } finally {
   1670             db.endTransaction();
   1671         }
   1672 
   1673         // We know that we succeeded becuase endTransaction throws if the transaction failed.
   1674         if (DBG) log("insertBulkContacts: added " + sum + " contacts!");
   1675         return true;
   1676     }
   1677 
   1678     // package scope for testing.
   1679     int updateBulkContacts(ContentValues values, String userWhere) {
   1680         ArrayList<String> usernames = values.getStringArrayList(Imps.Contacts.USERNAME);
   1681         ArrayList<String> nicknames = values.getStringArrayList(Imps.Contacts.NICKNAME);
   1682 
   1683         int usernameCount = usernames.size();
   1684         int nicknameCount = nicknames.size();
   1685 
   1686         if (usernameCount != nicknameCount) {
   1687             Log.e(LOG_TAG, "[ImProvider] updateBulkContacts: input bundle " +
   1688                     "username & nickname lists have diff. length!");
   1689             return 0;
   1690         }
   1691 
   1692         ArrayList<String> contactTypeArray = values.getStringArrayList(Imps.Contacts.TYPE);
   1693         ArrayList<String> subscriptionStatusArray =
   1694                 values.getStringArrayList(Imps.Contacts.SUBSCRIPTION_STATUS);
   1695         ArrayList<String> subscriptionTypeArray =
   1696                 values.getStringArrayList(Imps.Contacts.SUBSCRIPTION_TYPE);
   1697         ArrayList<String> quickContactArray = values.getStringArrayList(Imps.Contacts.QUICK_CONTACT);
   1698         ArrayList<String> rejectedArray = values.getStringArrayList(Imps.Contacts.REJECTED);
   1699         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1700 
   1701         db.beginTransaction();
   1702         int sum = 0;
   1703 
   1704         try {
   1705             Long provider = values.getAsLong(Imps.Contacts.PROVIDER);
   1706             Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
   1707 
   1708             ContentValues contactValues = new ContentValues();
   1709             contactValues.put(Imps.Contacts.PROVIDER, provider);
   1710             contactValues.put(Imps.Contacts.ACCOUNT, account);
   1711 
   1712             StringBuilder updateSelection = new StringBuilder();
   1713             String[] updateSelectionArgs = new String[1];
   1714 
   1715             for (int i=0; i<usernameCount; i++) {
   1716                 String username = usernames.get(i);
   1717                 String nickname = nicknames.get(i);
   1718                 int type = 0;
   1719                 int subscriptionStatus = 0;
   1720                 int subscriptionType = 0;
   1721                 int quickContact = 0;
   1722                 int rejected = 0;
   1723 
   1724                 try {
   1725                     type = Integer.parseInt(contactTypeArray.get(i));
   1726                     subscriptionStatus = Integer.parseInt(subscriptionStatusArray.get(i));
   1727                     subscriptionType = Integer.parseInt(subscriptionTypeArray.get(i));
   1728                     quickContact = Integer.parseInt(quickContactArray.get(i));
   1729                     rejected = Integer.parseInt(rejectedArray.get(i));
   1730                 } catch (NumberFormatException ex) {
   1731                     Log.e(LOG_TAG, "insertBulkContacts: caught " + ex);
   1732                 }
   1733 
   1734                 if (DBG) log("updateBulkContacts[" + i + "] username=" +
   1735                         username + ", nickname=" + nickname + ", type=" + type +
   1736                         ", subscriptionStatus=" + subscriptionStatus + ", subscriptionType=" +
   1737                         subscriptionType + ", qc=" + quickContact);
   1738 
   1739                 contactValues.put(Imps.Contacts.USERNAME, username);
   1740                 contactValues.put(Imps.Contacts.NICKNAME, nickname);
   1741                 contactValues.put(Imps.Contacts.TYPE, type);
   1742                 contactValues.put(Imps.Contacts.SUBSCRIPTION_STATUS, subscriptionStatus);
   1743                 contactValues.put(Imps.Contacts.SUBSCRIPTION_TYPE, subscriptionType);
   1744                 contactValues.put(Imps.Contacts.QUICK_CONTACT, quickContact);
   1745                 contactValues.put(Imps.Contacts.REJECTED, rejected);
   1746 
   1747                 // append username to the selection clause
   1748                 updateSelection.delete(0, updateSelection.length());
   1749                 updateSelection.append(userWhere);
   1750                 updateSelection.append(" AND ");
   1751                 updateSelection.append(Imps.Contacts.USERNAME);
   1752                 updateSelection.append("=?");
   1753 
   1754                 updateSelectionArgs[0] = username;
   1755 
   1756                 int numUpdated = db.update(TABLE_CONTACTS, contactValues,
   1757                         updateSelection.toString(), updateSelectionArgs);
   1758                 if (numUpdated == 0) {
   1759                     Log.e(LOG_TAG, "[ImProvider] updateBulkContacts: " +
   1760                             " update failed for selection = " + updateSelection);
   1761                 } else {
   1762                     sum += numUpdated;
   1763                 }
   1764 
   1765                 // yield the lock if anyone else is trying to
   1766                 // perform a db operation here.
   1767                 db.yieldIfContended();
   1768             }
   1769 
   1770             db.setTransactionSuccessful();
   1771         } finally {
   1772             db.endTransaction();
   1773         }
   1774 
   1775         if (DBG) log("updateBulkContacts: " + sum + " entries updated");
   1776         return sum;
   1777     }
   1778 
   1779     /**
   1780      * make sure the presence for all contacts of a given account is set to offline, and
   1781      * each contact has a presence row associated with it. However, this method does not remove
   1782      * presences for which the corresponding contacts no longer exist. That's probably ok since
   1783      * presence is kept in memory, so it won't stay around for too long. Here is the algorithm.
   1784      *
   1785      * 1. for all presence that have a corresponding contact, make it OFFLINE. This is one sqlite
   1786      *    call.
   1787      * 2. query for all the contacts that don't have a presence, and add a presence row for them.
   1788      *
   1789      * TODO simplify the presence management! The desire is to have a presence row for each
   1790      * TODO contact in the database, so later we can just call update() on the presence rows
   1791      * TODO instead of checking for the existence of presence first. The assumption is we get
   1792      * TODO presence updates much more frequently. However, the logic to maintain that goal is
   1793      * TODO overly complicated. One possible solution is to use insert_or_replace the presence rows
   1794      * TODO when updating the presence. That way we don't always need to maintain an empty presence
   1795      * TODO row for each contact.
   1796      *
   1797      * @param account the account of the contacts for which we want to create seed presence rows.
   1798      */
   1799     private void seedInitialPresenceByAccount(long account) {
   1800         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   1801         qb.setTables(TABLE_CONTACTS);
   1802         qb.setProjectionMap(sContactsProjectionMap);
   1803 
   1804         mQueryContactIdSelectionArgs1[0] = String.valueOf(account);
   1805 
   1806         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1807         db.beginTransaction();
   1808 
   1809         Cursor c = null;
   1810 
   1811         try {
   1812             ContentValues presenceValues = new ContentValues();
   1813             presenceValues.put(Imps.Presence.PRESENCE_STATUS, Imps.Presence.OFFLINE);
   1814             presenceValues.put(Imps.Presence.PRESENCE_CUSTOM_STATUS, "");
   1815 
   1816             // update all the presence for the account so they are offline
   1817             StringBuilder buf = new StringBuilder();
   1818             buf.append(Imps.Presence.CONTACT_ID);
   1819             buf.append(" in (select ");
   1820             buf.append(Imps.Contacts._ID);
   1821             buf.append(" from ");
   1822             buf.append(TABLE_CONTACTS);
   1823             buf.append(" where ");
   1824             buf.append(Imps.Contacts.ACCOUNT);
   1825             buf.append("=?) ");
   1826 
   1827             String selection = buf.toString();
   1828             if (DBG) log("seedInitialPresence: reset presence selection=" + selection);
   1829 
   1830             int count = db.update(TABLE_PRESENCE, presenceValues, selection,
   1831                     mQueryContactIdSelectionArgs1);
   1832             if (DBG) log("seedInitialPresence: reset " + count + " presence rows to OFFLINE");
   1833 
   1834             // for in-memory presence table, add a presence row for each contact that
   1835             // doesn't have a presence. in-memory presence table isn't reliable, and goes away
   1836             // when device reboot or IMProvider process dies, so we can't rely on each contact
   1837             // have a corresponding presence.
   1838             if (DBG) {
   1839                 log("seedInitialPresence: contacts_with_no_presence_selection => " +
   1840                         CONTACTS_WITH_NO_PRESENCE_SELECTION);
   1841             }
   1842 
   1843             c = qb.query(db,
   1844                     CONTACT_ID_PROJECTION,
   1845                     CONTACTS_WITH_NO_PRESENCE_SELECTION,
   1846                     mQueryContactIdSelectionArgs1,
   1847                     null, null, null, null);
   1848 
   1849             if (DBG) log("seedInitialPresence: found " + c.getCount() + " contacts w/o presence");
   1850 
   1851             count = 0;
   1852 
   1853             while (c.moveToNext()) {
   1854                 long id = c.getLong(CONTACT_ID_COLUMN);
   1855                 presenceValues.put(Imps.Presence.CONTACT_ID, id);
   1856 
   1857                 try {
   1858                     if (db.insert(TABLE_PRESENCE, null, presenceValues) > 0) {
   1859                         count++;
   1860                     }
   1861                 } catch (SQLiteConstraintException ex) {
   1862                     // we could possibly catch this exception, since there could be a presence
   1863                     // row with the same contact_id. That's fine, just ignore the error
   1864                     if (DBG) log("seedInitialPresence: insert presence for contact_id " + id +
   1865                             " failed, caught " + ex);
   1866                 }
   1867             }
   1868 
   1869             if (DBG) log("seedInitialPresence: added " + count + " new presence rows");
   1870 
   1871             db.setTransactionSuccessful();
   1872         } finally {
   1873             if (c != null) {
   1874                 c.close();
   1875             }
   1876             db.endTransaction();
   1877         }
   1878     }
   1879 
   1880     private int updateBulkPresence(ContentValues values, String userWhere, String[] whereArgs) {
   1881         ArrayList<String> usernames = values.getStringArrayList(Imps.Contacts.USERNAME);
   1882         int count = usernames.size();
   1883         Long account = values.getAsLong(Imps.Contacts.ACCOUNT);
   1884 
   1885         ArrayList<String> priorityArray = values.getStringArrayList(Imps.Presence.PRIORITY);
   1886         ArrayList<String> modeArray = values.getStringArrayList(Imps.Presence.PRESENCE_STATUS);
   1887         ArrayList<String> statusArray = values.getStringArrayList(
   1888                 Imps.Presence.PRESENCE_CUSTOM_STATUS);
   1889         ArrayList<String> clientTypeArray = values.getStringArrayList(Imps.Presence.CLIENT_TYPE);
   1890         ArrayList<String> resourceArray = values.getStringArrayList(Imps.Presence.JID_RESOURCE);
   1891 
   1892         // append username to the selection clause
   1893         StringBuilder buf = new StringBuilder();
   1894 
   1895         if (!TextUtils.isEmpty(userWhere)) {
   1896             buf.append(userWhere);
   1897             buf.append(" AND ");
   1898         }
   1899 
   1900         buf.append(Imps.Presence.CONTACT_ID);
   1901         buf.append(" in (select ");
   1902         buf.append(Imps.Contacts._ID);
   1903         buf.append(" from ");
   1904         buf.append(TABLE_CONTACTS);
   1905         buf.append(" where ");
   1906         buf.append(Imps.Contacts.ACCOUNT);
   1907         buf.append("=? AND ");
   1908 
   1909         // use username LIKE ? for case insensitive comparison
   1910         buf.append(Imps.Contacts.USERNAME);
   1911         buf.append(" LIKE ?) AND (");
   1912 
   1913         buf.append(Imps.Presence.PRIORITY);
   1914         buf.append("<=? OR ");
   1915         buf.append(Imps.Presence.PRIORITY);
   1916         buf.append(" IS NULL OR ");
   1917         buf.append(Imps.Presence.JID_RESOURCE);
   1918         buf.append("=?)");
   1919 
   1920         String selection = buf.toString();
   1921 
   1922         if (DBG) log("updateBulkPresence: selection => " + selection);
   1923 
   1924         int numArgs = (whereArgs != null ? whereArgs.length + 4 : 4);
   1925         String[] selectionArgs = new String[numArgs];
   1926         int selArgsIndex = 0;
   1927 
   1928         if (whereArgs != null) {
   1929             for (selArgsIndex=0; selArgsIndex<numArgs-1; selArgsIndex++) {
   1930                 selectionArgs[selArgsIndex] = whereArgs[selArgsIndex];
   1931             }
   1932         }
   1933 
   1934         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1935 
   1936         db.beginTransaction();
   1937         int sum = 0;
   1938 
   1939         try {
   1940             ContentValues presenceValues = new ContentValues();
   1941 
   1942             for (int i=0; i<count; i++) {
   1943                 String username = usernames.get(i);
   1944                 int priority = 0;
   1945                 int mode = 0;
   1946                 String status = statusArray.get(i);
   1947                 String jidResource = resourceArray == null ? "" : resourceArray.get(i);
   1948                 int clientType = Imps.Presence.CLIENT_TYPE_DEFAULT;
   1949 
   1950                 try {
   1951                     if (priorityArray != null) {
   1952                         priority = Integer.parseInt(priorityArray.get(i));
   1953                     }
   1954                     if (modeArray != null) {
   1955                         mode = Integer.parseInt(modeArray.get(i));
   1956                     }
   1957                     if (clientTypeArray != null) {
   1958                         clientType = Integer.parseInt(clientTypeArray.get(i));
   1959                     }
   1960                 } catch (NumberFormatException ex) {
   1961                     Log.e(LOG_TAG, "[ImProvider] updateBulkPresence: caught " + ex);
   1962                 }
   1963 
   1964                 /*
   1965                 if (DBG) {
   1966                     log("updateBulkPresence[" + i + "] username=" + username + ", priority=" +
   1967                             priority + ", mode=" + mode + ", status=" + status + ", resource=" +
   1968                             jidResource + ", clientType=" + clientType);
   1969                 }
   1970                 */
   1971 
   1972                 if (modeArray != null) {
   1973                     presenceValues.put(Imps.Presence.PRESENCE_STATUS, mode);
   1974                 }
   1975                 if (priorityArray != null) {
   1976                     presenceValues.put(Imps.Presence.PRIORITY, priority);
   1977                 }
   1978                 presenceValues.put(Imps.Presence.PRESENCE_CUSTOM_STATUS, status);
   1979                 if (clientTypeArray != null) {
   1980                     presenceValues.put(Imps.Presence.CLIENT_TYPE, clientType);
   1981                 }
   1982 
   1983                 if (!TextUtils.isEmpty(jidResource)) {
   1984                     presenceValues.put(Imps.Presence.JID_RESOURCE, jidResource);
   1985                 }
   1986 
   1987                 // fill in the selection args
   1988                 int idx = selArgsIndex;
   1989                 selectionArgs[idx++] = String.valueOf(account);
   1990                 selectionArgs[idx++] = username;
   1991                 selectionArgs[idx++] = String.valueOf(priority);
   1992                 selectionArgs[idx] = jidResource;
   1993 
   1994                 int numUpdated = db.update(TABLE_PRESENCE,
   1995                         presenceValues, selection, selectionArgs);
   1996                 if (numUpdated == 0) {
   1997                     Log.e(LOG_TAG, "[ImProvider] updateBulkPresence: failed for " + username);
   1998                 } else {
   1999                     sum += numUpdated;
   2000                 }
   2001 
   2002                 // yield the lock if anyone else is trying to
   2003                 // perform a db operation here.
   2004                 db.yieldIfContended();
   2005             }
   2006 
   2007             db.setTransactionSuccessful();
   2008         } finally {
   2009             db.endTransaction();
   2010         }
   2011 
   2012         if (DBG) log("updateBulkPresence: " + sum + " entries updated");
   2013         return sum;
   2014     }
   2015 
   2016     private Uri insertInternal(Uri url, ContentValues initialValues) {
   2017         Uri resultUri = null;
   2018         long rowID = 0;
   2019         long account = 0;
   2020         String contact = null;
   2021         long threadId = 0;
   2022 
   2023         boolean notifyContactListContentUri = false;
   2024         boolean notifyContactContentUri = false;
   2025         boolean notifyMessagesContentUri = false;
   2026         boolean notifyMessagesByContactContentUri = false;
   2027         boolean notifyMessagesByThreadIdContentUri = false;
   2028         boolean notifyProviderAccountContentUri = false;
   2029 
   2030         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2031         int match = mUrlMatcher.match(url);
   2032 
   2033         if (DBG) log("insert to " + url + ", match " + match);
   2034         switch (match) {
   2035             case MATCH_PROVIDERS:
   2036                 // Insert into the providers table
   2037                 rowID = db.insert(TABLE_PROVIDERS, "name", initialValues);
   2038                 if (rowID > 0) {
   2039                     resultUri = Uri.parse(Imps.Provider.CONTENT_URI + "/" + rowID);
   2040                 }
   2041                 notifyProviderAccountContentUri = true;
   2042                 break;
   2043 
   2044             case MATCH_ACCOUNTS:
   2045                 // Insert into the accounts table
   2046                 rowID = db.insert(TABLE_ACCOUNTS, "name", initialValues);
   2047                 if (rowID > 0) {
   2048                     resultUri = Uri.parse(Imps.Account.CONTENT_URI + "/" + rowID);
   2049                 }
   2050                 notifyProviderAccountContentUri = true;
   2051                 break;
   2052 
   2053             case MATCH_CONTACTS_BY_PROVIDER:
   2054                 appendValuesFromUrl(initialValues, url, Imps.Contacts.PROVIDER,
   2055                     Imps.Contacts.ACCOUNT);
   2056                 // fall through
   2057             case MATCH_CONTACTS:
   2058             case MATCH_CONTACTS_BAREBONE:
   2059                 // Insert into the contacts table
   2060                 rowID = db.insert(TABLE_CONTACTS, "username", initialValues);
   2061                 if (rowID > 0) {
   2062                     resultUri = Uri.parse(Imps.Contacts.CONTENT_URI + "/" + rowID);
   2063                 }
   2064 
   2065                 notifyContactContentUri = true;
   2066                 break;
   2067 
   2068             case MATCH_CONTACTS_BULK:
   2069                 if (insertBulkContacts(initialValues)) {
   2070                     // notify change using the "content://im/contacts" url,
   2071                     // so the change will be observed by listeners interested
   2072                     // in contacts changes.
   2073                     resultUri = Imps.Contacts.CONTENT_URI;
   2074                 }
   2075                 notifyContactContentUri = true;
   2076                 break;
   2077 
   2078             case MATCH_CONTACTLISTS_BY_PROVIDER:
   2079                 appendValuesFromUrl(initialValues, url, Imps.ContactList.PROVIDER,
   2080                         Imps.ContactList.ACCOUNT);
   2081                 // fall through
   2082             case MATCH_CONTACTLISTS:
   2083                 // Insert into the contactList table
   2084                 rowID = db.insert(TABLE_CONTACT_LIST, "name", initialValues);
   2085                 if (rowID > 0) {
   2086                     resultUri = Uri.parse(Imps.ContactList.CONTENT_URI + "/" + rowID);
   2087                 }
   2088                 notifyContactListContentUri = true;
   2089                 break;
   2090 
   2091             case MATCH_BLOCKEDLIST_BY_PROVIDER:
   2092                 appendValuesFromUrl(initialValues, url, Imps.BlockedList.PROVIDER,
   2093                     Imps.BlockedList.ACCOUNT);
   2094                 // fall through
   2095             case MATCH_BLOCKEDLIST:
   2096                 // Insert into the blockedList table
   2097                 rowID = db.insert(TABLE_BLOCKED_LIST, "username", initialValues);
   2098                 if (rowID > 0) {
   2099                     resultUri = Uri.parse(Imps.BlockedList.CONTENT_URI + "/" + rowID);
   2100                 }
   2101 
   2102                 break;
   2103 
   2104             case MATCH_CONTACTS_ETAGS:
   2105                 rowID = db.replace(TABLE_CONTACTS_ETAG, Imps.ContactsEtag.ETAG, initialValues);
   2106                 if (rowID > 0) {
   2107                     resultUri = Uri.parse(Imps.ContactsEtag.CONTENT_URI + "/" + rowID);
   2108                 }
   2109                 break;
   2110 
   2111             case MATCH_MESSAGES_BY_CONTACT:
   2112                 String accountStr = decodeURLSegment(url.getPathSegments().get(1));
   2113                 try {
   2114                     account = Long.parseLong(accountStr);
   2115                 } catch (NumberFormatException ex) {
   2116                     throw new IllegalArgumentException();
   2117                 }
   2118 
   2119                 contact = decodeURLSegment(url.getPathSegments().get(2));
   2120                 initialValues.put(Imps.Messages.THREAD_ID, getContactId(db, accountStr, contact));
   2121 
   2122                 notifyMessagesContentUri = true;
   2123 
   2124                 // Insert into the messages table.
   2125                 rowID = db.insert(TABLE_MESSAGES, "thread_id", initialValues);
   2126                 if (rowID > 0) {
   2127                     resultUri = Uri.parse(Imps.Messages.CONTENT_URI + "/" + rowID);
   2128                 }
   2129 
   2130                 break;
   2131 
   2132             case MATCH_MESSAGES_BY_THREAD_ID:
   2133                 appendValuesFromUrl(initialValues, url, Imps.Messages.THREAD_ID);
   2134                 // fall through
   2135 
   2136             case MATCH_MESSAGES:
   2137                 // Insert into the messages table.
   2138                 notifyMessagesContentUri = true;
   2139                 rowID = db.insert(TABLE_MESSAGES, "thread_id", initialValues);
   2140                 if (rowID > 0) {
   2141                     resultUri = Uri.parse(Imps.Messages.CONTENT_URI + "/" + rowID);
   2142                 }
   2143 
   2144                 break;
   2145 
   2146             case MATCH_OTR_MESSAGES_BY_CONTACT:
   2147                 String accountStr2 = decodeURLSegment(url.getPathSegments().get(1));
   2148 
   2149                 try {
   2150                     account = Long.parseLong(accountStr2);
   2151                 } catch (NumberFormatException ex) {
   2152                     throw new IllegalArgumentException();
   2153                 }
   2154 
   2155                 contact = decodeURLSegment(url.getPathSegments().get(2));
   2156                 initialValues.put(Imps.Messages.THREAD_ID, getContactId(db, accountStr2, contact));
   2157 
   2158                 notifyMessagesByContactContentUri = true;
   2159 
   2160                 // Insert into the in-memory messages table.
   2161                 rowID = db.insert(TABLE_IN_MEMORY_MESSAGES, "thread_id", initialValues);
   2162                 if (rowID > 0) {
   2163                     resultUri = Uri.parse(Imps.Messages.OTR_MESSAGES_CONTENT_URI + "/" + rowID);
   2164                 }
   2165 
   2166                 break;
   2167 
   2168             case MATCH_OTR_MESSAGES_BY_THREAD_ID:
   2169                 try {
   2170                     threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
   2171                 } catch (NumberFormatException ex) {
   2172                     throw new IllegalArgumentException();
   2173                 }
   2174 
   2175                 initialValues.put(Imps.Messages.THREAD_ID, threadId);
   2176 
   2177                 notifyMessagesByThreadIdContentUri = true;
   2178                 // fall through
   2179 
   2180             case MATCH_OTR_MESSAGES:
   2181                 // Insert into the messages table.
   2182                 rowID = db.insert(TABLE_IN_MEMORY_MESSAGES, "thread_id", initialValues);
   2183                 if (rowID > 0) {
   2184                     resultUri = Uri.parse(Imps.Messages.OTR_MESSAGES_CONTENT_URI + "/" + rowID);
   2185                 }
   2186 
   2187                 break;
   2188 
   2189             case MATCH_INVITATIONS:
   2190                 rowID = db.insert(TABLE_INVITATIONS, null, initialValues);
   2191                 if (rowID > 0) {
   2192                     resultUri = Uri.parse(Imps.Invitation.CONTENT_URI + "/" + rowID);
   2193                 }
   2194                 break;
   2195 
   2196             case MATCH_GROUP_MEMBERS:
   2197                 rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
   2198                 if (rowID > 0) {
   2199                     resultUri = Uri.parse(Imps.GroupMembers.CONTENT_URI + "/" + rowID);
   2200                 }
   2201                 break;
   2202 
   2203             case MATCH_GROUP_MEMBERS_BY_GROUP:
   2204                 appendValuesFromUrl(initialValues, url, Imps.GroupMembers.GROUP);
   2205                 rowID = db.insert(TABLE_GROUP_MEMBERS, "nickname", initialValues);
   2206                 if (rowID > 0) {
   2207                     resultUri = Uri.parse(Imps.GroupMembers.CONTENT_URI + "/" + rowID);
   2208                 }
   2209                 break;
   2210 
   2211             case MATCH_AVATAR_BY_PROVIDER:
   2212                 appendValuesFromUrl(initialValues, url, Imps.Avatars.PROVIDER, Imps.Avatars.ACCOUNT);
   2213                 // fall through
   2214             case MATCH_AVATARS:
   2215                 // Insert into the avatars table
   2216                 rowID = db.replace(TABLE_AVATARS, "contact", initialValues);
   2217                 if (rowID > 0) {
   2218                     resultUri = Uri.parse(Imps.Avatars.CONTENT_URI + "/" + rowID);
   2219                 }
   2220                 break;
   2221 
   2222             case MATCH_CHATS_ID:
   2223                 appendValuesFromUrl(initialValues, url, Imps.Chats.CONTACT_ID);
   2224                 // fall through
   2225             case MATCH_CHATS:
   2226                 // Insert into the chats table
   2227                 initialValues.put(Imps.Chats.SHORTCUT, -1);
   2228                 rowID = db.replace(TABLE_CHATS, Imps.Chats.CONTACT_ID, initialValues);
   2229                 if (rowID > 0) {
   2230                     resultUri = Uri.parse(Imps.Chats.CONTENT_URI + "/" + rowID);
   2231                     addToQuickSwitch(rowID);
   2232                 }
   2233                 notifyContactContentUri = true;
   2234                 break;
   2235 
   2236             case MATCH_PRESENCE:
   2237                 rowID = db.replace(TABLE_PRESENCE, null, initialValues);
   2238                 if (rowID > 0) {
   2239                     resultUri = Uri.parse(Imps.Presence.CONTENT_URI + "/" + rowID);
   2240                 }
   2241                 notifyContactContentUri = true;
   2242                 break;
   2243 
   2244             case MATCH_PRESENCE_SEED_BY_ACCOUNT:
   2245                 try {
   2246                     seedInitialPresenceByAccount(Long.parseLong(url.getLastPathSegment()));
   2247                     resultUri = Imps.Presence.CONTENT_URI;
   2248                 } catch (NumberFormatException ex) {
   2249                     throw new IllegalArgumentException();
   2250                 }
   2251                 break;
   2252 
   2253             case MATCH_SESSIONS_BY_PROVIDER:
   2254                 appendValuesFromUrl(initialValues, url, Imps.SessionCookies.PROVIDER,
   2255                         Imps.SessionCookies.ACCOUNT);
   2256                 // fall through
   2257             case MATCH_SESSIONS:
   2258                 rowID = db.insert(TABLE_SESSION_COOKIES, null, initialValues);
   2259                 if(rowID > 0) {
   2260                     resultUri = Uri.parse(Imps.SessionCookies.CONTENT_URI + "/" + rowID);
   2261                 }
   2262                 break;
   2263 
   2264             case MATCH_PROVIDER_SETTINGS:
   2265                 rowID = db.replace(TABLE_PROVIDER_SETTINGS, null, initialValues);
   2266                 if (rowID > 0) {
   2267                     resultUri = Uri.parse(Imps.ProviderSettings.CONTENT_URI + "/" + rowID);
   2268                 }
   2269                 break;
   2270 
   2271             case MATCH_ACCOUNTS_STATUS:
   2272                 rowID = db.replace(TABLE_ACCOUNT_STATUS, null, initialValues);
   2273                 if (rowID > 0) {
   2274                     resultUri = Uri.parse(Imps.AccountStatus.CONTENT_URI + "/" + rowID);
   2275                 }
   2276                 notifyProviderAccountContentUri = true;
   2277                 break;
   2278 
   2279             case MATCH_BRANDING_RESOURCE_MAP_CACHE:
   2280                 rowID = db.insert(TABLE_BRANDING_RESOURCE_MAP_CACHE, null, initialValues);
   2281                 if (rowID > 0) {
   2282                     resultUri = Uri.parse(Imps.BrandingResourceMapCache.CONTENT_URI + "/" + rowID);
   2283                 }
   2284                 break;
   2285 
   2286             // mcs/rmq stuff
   2287             case MATCH_OUTGOING_RMQ_MESSAGES:
   2288                 rowID = db.insert(TABLE_OUTGOING_RMQ_MESSAGES, null, initialValues);
   2289                 if (rowID > 0) {
   2290                     resultUri = Uri.parse(Imps.OutgoingRmq.CONTENT_URI + "/" + rowID);
   2291                 }
   2292                 break;
   2293 
   2294             case MATCH_LAST_RMQ_ID:
   2295                 rowID = db.replace(TABLE_LAST_RMQ_ID, null, initialValues);
   2296                 if (rowID > 0) {
   2297                     resultUri = Uri.parse(Imps.LastRmqId.CONTENT_URI + "/" + rowID);
   2298                 }
   2299                 break;
   2300 
   2301             case MATCH_S2D_RMQ_IDS:
   2302                 rowID = db.insert(TABLE_S2D_RMQ_IDS, null, initialValues);
   2303                 if (rowID > 0) {
   2304                     resultUri = Uri.parse(Imps.ServerToDeviceRmqIds.CONTENT_URI + "/" + rowID);
   2305                 }
   2306                 break;
   2307 
   2308             default:
   2309                 throw new UnsupportedOperationException("Cannot insert into URL: " + url);
   2310         }
   2311         // TODO: notify the data change observer?
   2312 
   2313         if (resultUri != null) {
   2314             ContentResolver resolver = getContext().getContentResolver();
   2315 
   2316             // In most case, we query contacts with presence and chats joined, thus
   2317             // we should also notify that contacts changes when presence or chats changed.
   2318             if (notifyContactContentUri) {
   2319                 resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
   2320             }
   2321 
   2322             if (notifyContactListContentUri) {
   2323                 resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
   2324             }
   2325 
   2326             if (notifyMessagesContentUri) {
   2327                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2328             }
   2329 
   2330             if (notifyMessagesByContactContentUri) {
   2331                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2332                 resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
   2333             }
   2334 
   2335             if (notifyMessagesByThreadIdContentUri) {
   2336                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2337                 resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
   2338             }
   2339 
   2340             if (notifyProviderAccountContentUri) {
   2341                 if (DBG) log("notify insert for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
   2342                 resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
   2343             }
   2344         }
   2345         return resultUri;
   2346     }
   2347 
   2348     private void appendValuesFromUrl(ContentValues values, Uri url, String...columns){
   2349         if(url.getPathSegments().size() <= columns.length) {
   2350             throw new IllegalArgumentException("Not enough values in url");
   2351         }
   2352         for(int i = 0; i < columns.length; i++){
   2353             if(values.containsKey(columns[i])){
   2354                 throw new UnsupportedOperationException("Cannot override the value for " + columns[i]);
   2355             }
   2356             values.put(columns[i], decodeURLSegment(url.getPathSegments().get(i + 1)));
   2357         }
   2358     }
   2359 
   2360     private long getContactId(final SQLiteDatabase db,
   2361                               final String accountId, final String contact) {
   2362         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   2363         qb.setTables(TABLE_CONTACTS);
   2364         qb.setProjectionMap(sContactsProjectionMap);
   2365 
   2366         mQueryContactIdSelectionArgs2[0] = accountId;
   2367         mQueryContactIdSelectionArgs2[1] = contact;
   2368 
   2369         Cursor c = qb.query(db,
   2370                 CONTACT_ID_PROJECTION,
   2371                 CONTACT_ID_QUERY_SELECTION,
   2372                 mQueryContactIdSelectionArgs2,
   2373                 null, null, null, null);
   2374 
   2375         long contactId = 0;
   2376 
   2377         try {
   2378             if (c.moveToFirst()) {
   2379                 contactId = c.getLong(CONTACT_ID_COLUMN);
   2380             }
   2381         } finally {
   2382             c.close();
   2383         }
   2384 
   2385         return contactId;
   2386     }
   2387 
   2388     //  Quick-switch management
   2389     //  The chat UI provides slots (0, 9, .., 1) for the first 10 chats.  This allows you to
   2390     //  quickly switch between these chats by chording menu+#.  We number from the right end of
   2391     //  the number row and move leftward to make an easier two-hand chord with the menu button
   2392     //  on the left side of the keyboard.
   2393     private void addToQuickSwitch(long newRow) {
   2394         //  Since there are fewer than 10, there must be an empty slot.  Let's find it.
   2395         int slot = findEmptyQuickSwitchSlot();
   2396 
   2397         if (slot == -1) {
   2398             return;
   2399         }
   2400 
   2401         updateSlotForChat(newRow, slot);
   2402     }
   2403 
   2404     //  If there are more than 10 chats and one with a quick switch slot ends then pick a chat
   2405     //  that doesn't have a slot and have it inhabit the newly emptied slot.
   2406     private void backfillQuickSwitchSlots() {
   2407         //  Find all the chats without a quick switch slot, and order
   2408         Cursor c = query(Imps.Chats.CONTENT_URI,
   2409             BACKFILL_PROJECTION,
   2410             Imps.Chats.SHORTCUT + "=-1", null, Imps.Chats.LAST_MESSAGE_DATE + " DESC");
   2411 
   2412         try {
   2413             if (c.getCount() < 1) {
   2414                 return;
   2415             }
   2416 
   2417             int slot = findEmptyQuickSwitchSlot();
   2418 
   2419             if (slot != -1) {
   2420                 c.moveToFirst();
   2421 
   2422                 long id = c.getLong(c.getColumnIndex(Imps.Chats._ID));
   2423 
   2424                 updateSlotForChat(id, slot);
   2425             }
   2426         } finally {
   2427             c.close();
   2428         }
   2429     }
   2430 
   2431     private int updateSlotForChat(long chatId, int slot) {
   2432         ContentValues values = new ContentValues();
   2433 
   2434         values.put(Imps.Chats.SHORTCUT, slot);
   2435 
   2436         return update(Imps.Chats.CONTENT_URI, values, Imps.Chats._ID + "=?",
   2437             new String[] { Long.toString(chatId) });
   2438     }
   2439 
   2440     private int findEmptyQuickSwitchSlot() {
   2441         Cursor c = queryInternal(Imps.Chats.CONTENT_URI, FIND_SHORTCUT_PROJECTION, null, null, null);
   2442         final int N = c.getCount();
   2443 
   2444         try {
   2445             //  If there are 10 or more chats then all the quick switch slots are already filled
   2446             if (N >= 10) {
   2447                 return -1;
   2448             }
   2449 
   2450             int slots = 0;
   2451             int column = c.getColumnIndex(Imps.Chats.SHORTCUT);
   2452 
   2453             //  The map is here because numbers go from 0-9, but we want to assign slots in
   2454             //  0, 9, 8, ..., 1 order to match the right-to-left reading of the number row
   2455             //  on the keyboard.
   2456             int[] map = new int[] { 0, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
   2457 
   2458             //  Mark all the slots that are in use
   2459             //  The shortcuts represent actual keyboard number row keys, and not ordinals.
   2460             //  So 7 would mean the shortcut is the 7 key on the keyboard and NOT the 7th
   2461             //  shortcut.  The passing of slot through map[] below maps these keyboard key
   2462             //  shortcuts into an ordinal bit position in the 'slots' bitfield.
   2463             for (c.moveToFirst(); ! c.isAfterLast(); c.moveToNext()) {
   2464                 int slot = c.getInt(column);
   2465 
   2466                 if (slot != -1) {
   2467                     slots |= (1 << map[slot]);
   2468                 }
   2469             }
   2470 
   2471             //  Try to find an empty one
   2472             //  As we exit this, the push of i through map[] maps the ordinal bit position
   2473             //  in the 'slots' bitfield onto a key on the number row of the device keyboard.
   2474             //  The keyboard key is what is used to designate the shortcut.
   2475             for (int i = 0; i < 10; i++) {
   2476                 if ((slots & (1 << i)) == 0) {
   2477                     return map[i];
   2478                 }
   2479             }
   2480 
   2481             return -1;
   2482         } finally {
   2483             c.close();
   2484         }
   2485     }
   2486 
   2487     /**
   2488      * manual trigger for deleting contacts
   2489      */
   2490     private static final String DELETE_PRESENCE_SELECTION =
   2491             Imps.Presence.CONTACT_ID + " in (select " +
   2492             PRESENCE_CONTACT_ID + " from " + TABLE_PRESENCE + " left outer join " + TABLE_CONTACTS +
   2493             " on " + PRESENCE_CONTACT_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
   2494 
   2495     private static final String CHATS_CONTACT_ID = TABLE_CHATS + '.' + Imps.Chats.CONTACT_ID;
   2496     private static final String DELETE_CHATS_SELECTION = Imps.Chats.CONTACT_ID + " in (select "+
   2497             CHATS_CONTACT_ID + " from " + TABLE_CHATS + " left outer join " + TABLE_CONTACTS +
   2498             " on " + CHATS_CONTACT_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
   2499 
   2500     private static final String GROUP_MEMBER_ID = TABLE_GROUP_MEMBERS + '.' + Imps.GroupMembers.GROUP;
   2501     private static final String DELETE_GROUP_MEMBER_SELECTION =
   2502             Imps.GroupMembers.GROUP + " in (select "+
   2503             GROUP_MEMBER_ID + " from " + TABLE_GROUP_MEMBERS + " left outer join " + TABLE_CONTACTS +
   2504             " on " + GROUP_MEMBER_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
   2505 
   2506     private static final String GROUP_MESSAGES_ID = TABLE_MESSAGES + '.' + Imps.Messages.THREAD_ID;
   2507     private static final String DELETE_GROUP_MESSAGES_SELECTION =
   2508             Imps.Messages.THREAD_ID + " in (select "+ GROUP_MESSAGES_ID + " from " +
   2509                     TABLE_MESSAGES + " left outer join " + TABLE_CONTACTS + " on " +
   2510                     GROUP_MESSAGES_ID + '=' + CONTACT_ID + " where " + CONTACT_ID + " IS NULL)";
   2511 
   2512     private void performContactRemovalCleanup(long contactId) {
   2513         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2514 
   2515         if (contactId > 0) {
   2516             StringBuilder buf = new StringBuilder();
   2517 
   2518             // delete presence
   2519             buf.append(Imps.Presence.CONTACT_ID).append('=').append(contactId);
   2520             deleteWithSelection(db, TABLE_PRESENCE, buf.toString(), null);
   2521 
   2522             // delete group memebers
   2523             buf.delete(0, buf.length());
   2524             buf.append(Imps.GroupMembers.GROUP).append('=').append(contactId);
   2525             deleteWithSelection(db, TABLE_GROUP_MEMBERS, buf.toString(), null);
   2526         } else {
   2527             // delete presence
   2528             deleteWithSelection(db, TABLE_PRESENCE, DELETE_PRESENCE_SELECTION, null);
   2529 
   2530             // delete group members
   2531             deleteWithSelection(db, TABLE_GROUP_MEMBERS, DELETE_GROUP_MEMBER_SELECTION, null);
   2532         }
   2533     }
   2534 
   2535     private void deleteWithSelection(SQLiteDatabase db, String tableName,
   2536             String selection, String[] selectionArgs) {
   2537         if (DBG) log("deleteWithSelection: table " + tableName + ", selection => " + selection);
   2538         int count = db.delete(tableName, selection, selectionArgs);
   2539         if (DBG) log("deleteWithSelection: deleted " + count + " rows");
   2540     }
   2541 
   2542     private String buildContactIdSelection(String columnName, String contactSelection) {
   2543         StringBuilder buf = new StringBuilder();
   2544 
   2545         buf.append(columnName);
   2546         buf.append(" in (select ");
   2547         buf.append(Imps.Contacts._ID);
   2548         buf.append(" from ");
   2549         buf.append(TABLE_CONTACTS);
   2550         buf.append(" where ");
   2551         buf.append(contactSelection);
   2552         buf.append(")");
   2553 
   2554         return buf.toString();
   2555     }
   2556 
   2557      private int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
   2558         String tableToChange;
   2559 
   2560         // In some cases a given url requires that we delete rows from more than one
   2561         // table.  The motivating example is deleting messages from both the on disk
   2562         // and in memory messages tables.
   2563         String tableToChange2 = null;
   2564         String idColumnName = null;
   2565         String changedItemId = null;
   2566         String provider = null;
   2567         String accountStr = null;
   2568         long account = 0;
   2569         String contact = null;
   2570         long threadId = 0;
   2571 
   2572         StringBuilder whereClause = new StringBuilder();
   2573         if(userWhere != null) {
   2574             whereClause.append(userWhere);
   2575         }
   2576 
   2577         boolean notifyMessagesContentUri = false;
   2578         boolean notifyMessagesByContactContentUri = false;
   2579         boolean notifyMessagesByThreadIdContentUri = false;
   2580         boolean notifyContactListContentUri = false;
   2581         boolean notifyProviderAccountContentUri = false;
   2582         int match = mUrlMatcher.match(url);
   2583 
   2584         boolean contactDeleted = false;
   2585         long deletedContactId = 0;
   2586 
   2587         boolean backfillQuickSwitchSlots = false;
   2588 
   2589         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2590 
   2591         switch (match) {
   2592             case MATCH_PROVIDERS:
   2593                 tableToChange = TABLE_PROVIDERS;
   2594                 notifyProviderAccountContentUri = true;
   2595                 break;
   2596 
   2597             case MATCH_ACCOUNTS_BY_ID:
   2598                 changedItemId = url.getPathSegments().get(1);
   2599                 // fall through
   2600             case MATCH_ACCOUNTS:
   2601                 tableToChange = TABLE_ACCOUNTS;
   2602                 notifyProviderAccountContentUri = true;
   2603                 break;
   2604 
   2605             case MATCH_ACCOUNT_STATUS:
   2606                 changedItemId = url.getPathSegments().get(1);
   2607                 // fall through
   2608             case MATCH_ACCOUNTS_STATUS:
   2609                 tableToChange = TABLE_ACCOUNT_STATUS;
   2610                 notifyProviderAccountContentUri = true;
   2611                 break;
   2612 
   2613             case MATCH_CONTACTS:
   2614             case MATCH_CONTACTS_BAREBONE:
   2615                 tableToChange = TABLE_CONTACTS;
   2616                 contactDeleted = true;
   2617                 break;
   2618 
   2619             case MATCH_CONTACT:
   2620                 tableToChange = TABLE_CONTACTS;
   2621                 changedItemId = url.getPathSegments().get(1);
   2622 
   2623                 try {
   2624                     deletedContactId = Long.parseLong(changedItemId);
   2625                 } catch (NumberFormatException ex) {
   2626                     throw new IllegalArgumentException();
   2627                 }
   2628 
   2629                 contactDeleted = true;
   2630                 break;
   2631 
   2632             case MATCH_CONTACTS_BY_PROVIDER:
   2633                 tableToChange = TABLE_CONTACTS;
   2634                 appendWhere(whereClause, Imps.Contacts.ACCOUNT, "=", url.getPathSegments().get(2));
   2635                 contactDeleted = true;
   2636                 break;
   2637 
   2638             case MATCH_CONTACTLISTS_BY_PROVIDER:
   2639                 appendWhere(whereClause, Imps.ContactList.ACCOUNT, "=",
   2640                         url.getPathSegments().get(2));
   2641                 // fall through
   2642             case MATCH_CONTACTLISTS:
   2643                 tableToChange = TABLE_CONTACT_LIST;
   2644                 notifyContactListContentUri = true;
   2645                 break;
   2646 
   2647             case MATCH_CONTACTLIST:
   2648                 tableToChange = TABLE_CONTACT_LIST;
   2649                 changedItemId = url.getPathSegments().get(1);
   2650                 break;
   2651 
   2652             case MATCH_BLOCKEDLIST:
   2653                 tableToChange = TABLE_BLOCKED_LIST;
   2654                 break;
   2655 
   2656             case MATCH_BLOCKEDLIST_BY_PROVIDER:
   2657                 tableToChange = TABLE_BLOCKED_LIST;
   2658                 appendWhere(whereClause, Imps.BlockedList.ACCOUNT, "=", url.getPathSegments().get(2));
   2659                 break;
   2660 
   2661             case MATCH_CONTACTS_ETAGS:
   2662                 tableToChange = TABLE_CONTACTS_ETAG;
   2663                 break;
   2664 
   2665             case MATCH_CONTACTS_ETAG:
   2666                 tableToChange = TABLE_CONTACTS_ETAG;
   2667                 changedItemId = url.getPathSegments().get(1);
   2668                 break;
   2669 
   2670             case MATCH_MESSAGES:
   2671                 tableToChange = TABLE_MESSAGES;
   2672                 break;
   2673 
   2674             case MATCH_MESSAGES_BY_CONTACT:
   2675                 tableToChange = TABLE_MESSAGES;
   2676                 tableToChange2 = TABLE_IN_MEMORY_MESSAGES;
   2677 
   2678                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   2679                 try {
   2680                     account = Long.parseLong(accountStr);
   2681                 } catch (NumberFormatException ex) {
   2682                     throw new IllegalArgumentException();
   2683                 }
   2684 
   2685                 contact = decodeURLSegment(url.getPathSegments().get(2));
   2686                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
   2687                         getContactId(db, accountStr, contact));
   2688 
   2689                 notifyMessagesContentUri = true;
   2690                 break;
   2691 
   2692             case MATCH_MESSAGES_BY_THREAD_ID:
   2693                 tableToChange = TABLE_MESSAGES;
   2694                 tableToChange2 = TABLE_IN_MEMORY_MESSAGES;
   2695 
   2696                 try {
   2697                     threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
   2698                 } catch (NumberFormatException ex) {
   2699                     throw new IllegalArgumentException();
   2700                 }
   2701 
   2702                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
   2703 
   2704                 notifyMessagesContentUri = true;
   2705                 break;
   2706 
   2707             case MATCH_MESSAGES_BY_PROVIDER:
   2708                 tableToChange = TABLE_MESSAGES;
   2709 
   2710                 provider = decodeURLSegment(url.getPathSegments().get(1));
   2711                 appendWhere(whereClause, buildContactIdSelection(Imps.Messages.THREAD_ID,
   2712                         Imps.Contacts.PROVIDER + "='" + provider + "'"));
   2713 
   2714                 notifyMessagesContentUri = true;
   2715                 break;
   2716 
   2717             case MATCH_MESSAGES_BY_ACCOUNT:
   2718                 tableToChange = TABLE_MESSAGES;
   2719 
   2720                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   2721                 appendWhere(whereClause, buildContactIdSelection(Imps.Messages.THREAD_ID,
   2722                         Imps.Contacts.ACCOUNT + "='" + accountStr + "'"));
   2723 
   2724                 notifyMessagesContentUri = true;
   2725                 break;
   2726 
   2727             case MATCH_MESSAGE:
   2728                 tableToChange = TABLE_MESSAGES;
   2729                 changedItemId = url.getPathSegments().get(1);
   2730                 notifyMessagesContentUri = true;
   2731                 break;
   2732 
   2733             case MATCH_OTR_MESSAGES:
   2734                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2735                 break;
   2736 
   2737             case MATCH_OTR_MESSAGES_BY_CONTACT:
   2738                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2739 
   2740                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   2741                 try {
   2742                     account = Long.parseLong(accountStr);
   2743                 } catch (NumberFormatException ex) {
   2744                     throw new IllegalArgumentException();
   2745                 }
   2746 
   2747                 contact = decodeURLSegment(url.getPathSegments().get(2));
   2748                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
   2749                         getContactId(db, accountStr, contact));
   2750 
   2751                 notifyMessagesByContactContentUri = true;
   2752                 break;
   2753 
   2754             case MATCH_OTR_MESSAGES_BY_THREAD_ID:
   2755                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2756 
   2757                 try {
   2758                     threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
   2759                 } catch (NumberFormatException ex) {
   2760                     throw new IllegalArgumentException();
   2761                 }
   2762 
   2763                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
   2764 
   2765                 notifyMessagesByThreadIdContentUri = true;
   2766                 break;
   2767 
   2768             case MATCH_OTR_MESSAGES_BY_PROVIDER:
   2769                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2770 
   2771                 provider = decodeURLSegment(url.getPathSegments().get(1));
   2772                 appendWhere(whereClause, buildContactIdSelection(Imps.Messages.THREAD_ID,
   2773                         Imps.Contacts.PROVIDER + "='" + provider + "'"));
   2774 
   2775                 if (DBG) log("delete (MATCH_OTR_MESSAGES_BY_PROVIDER) sel => " + whereClause);
   2776                 notifyMessagesContentUri = true;
   2777                 break;
   2778 
   2779             case MATCH_OTR_MESSAGES_BY_ACCOUNT:
   2780                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2781 
   2782                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   2783                 appendWhere(whereClause, buildContactIdSelection(Imps.Messages.THREAD_ID,
   2784                         Imps.Contacts.ACCOUNT + "='" + accountStr + "'"));
   2785 
   2786                 if (DBG) log("delete (MATCH_OTR_MESSAGES_BY_ACCOUNT) sel => " + whereClause);
   2787                 notifyMessagesContentUri = true;
   2788                 break;
   2789 
   2790             case MATCH_OTR_MESSAGE:
   2791                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   2792                 changedItemId = url.getPathSegments().get(1);
   2793                 notifyMessagesContentUri = true;
   2794                 break;
   2795 
   2796             case MATCH_GROUP_MEMBERS:
   2797                 tableToChange = TABLE_GROUP_MEMBERS;
   2798                 break;
   2799 
   2800             case MATCH_GROUP_MEMBERS_BY_GROUP:
   2801                 tableToChange = TABLE_GROUP_MEMBERS;
   2802                 appendWhere(whereClause, Imps.GroupMembers.GROUP, "=", url.getPathSegments().get(1));
   2803                 break;
   2804 
   2805             case MATCH_INVITATIONS:
   2806                 tableToChange = TABLE_INVITATIONS;
   2807                 break;
   2808 
   2809             case MATCH_INVITATION:
   2810                 tableToChange = TABLE_INVITATIONS;
   2811                 changedItemId = url.getPathSegments().get(1);
   2812                 break;
   2813 
   2814             case MATCH_AVATARS:
   2815                 tableToChange = TABLE_AVATARS;
   2816                 break;
   2817 
   2818             case MATCH_AVATAR:
   2819                 tableToChange = TABLE_AVATARS;
   2820                 changedItemId = url.getPathSegments().get(1);
   2821                 break;
   2822 
   2823             case MATCH_AVATAR_BY_PROVIDER:
   2824                 tableToChange = TABLE_AVATARS;
   2825                 changedItemId = url.getPathSegments().get(2);
   2826                 idColumnName = Imps.Avatars.ACCOUNT;
   2827                 break;
   2828 
   2829             case MATCH_CHATS:
   2830                 tableToChange = TABLE_CHATS;
   2831                 backfillQuickSwitchSlots = true;
   2832                 break;
   2833 
   2834             case MATCH_CHATS_BY_ACCOUNT:
   2835                 tableToChange = TABLE_CHATS;
   2836 
   2837                 accountStr = decodeURLSegment(url.getLastPathSegment());
   2838                 appendWhere(whereClause, buildContactIdSelection(Imps.Chats.CONTACT_ID,
   2839                         Imps.Contacts.ACCOUNT + "='" + accountStr + "'"));
   2840 
   2841                 if (DBG) log("delete (MATCH_CHATS_BY_ACCOUNT) sel => " + whereClause);
   2842 
   2843                 changedItemId = null;
   2844                 break;
   2845 
   2846             case MATCH_CHATS_ID:
   2847                 tableToChange = TABLE_CHATS;
   2848                 changedItemId = url.getPathSegments().get(1);
   2849                 idColumnName = Imps.Chats.CONTACT_ID;
   2850                 break;
   2851 
   2852             case MATCH_PRESENCE:
   2853                 tableToChange = TABLE_PRESENCE;
   2854                 break;
   2855 
   2856             case MATCH_PRESENCE_ID:
   2857                 tableToChange = TABLE_PRESENCE;
   2858                 changedItemId = url.getPathSegments().get(1);
   2859                 idColumnName = Imps.Presence.CONTACT_ID;
   2860                 break;
   2861 
   2862             case MATCH_PRESENCE_BY_ACCOUNT:
   2863                 tableToChange = TABLE_PRESENCE;
   2864 
   2865                 accountStr = decodeURLSegment(url.getLastPathSegment());
   2866                 appendWhere(whereClause, buildContactIdSelection(Imps.Presence.CONTACT_ID,
   2867                         Imps.Contacts.ACCOUNT + "='" + accountStr + "'"));
   2868 
   2869                 if (DBG) log("delete (MATCH_PRESENCE_BY_ACCOUNT): sel => " + whereClause);
   2870                 changedItemId = null;
   2871                 break;
   2872 
   2873             case MATCH_SESSIONS:
   2874                 tableToChange = TABLE_SESSION_COOKIES;
   2875                 break;
   2876 
   2877             case MATCH_SESSIONS_BY_PROVIDER:
   2878                 tableToChange = TABLE_SESSION_COOKIES;
   2879                 changedItemId = url.getPathSegments().get(2);
   2880                 idColumnName = Imps.SessionCookies.ACCOUNT;
   2881                 break;
   2882 
   2883             case MATCH_PROVIDER_SETTINGS_BY_ID:
   2884                 tableToChange = TABLE_PROVIDER_SETTINGS;
   2885                 changedItemId = url.getPathSegments().get(1);
   2886                 idColumnName = Imps.ProviderSettings.PROVIDER;
   2887                 break;
   2888 
   2889             case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
   2890                 tableToChange = TABLE_PROVIDER_SETTINGS;
   2891 
   2892                 String providerId = url.getPathSegments().get(1);
   2893                 String name = url.getPathSegments().get(2);
   2894 
   2895                 appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", providerId);
   2896                 appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", name);
   2897                 break;
   2898 
   2899             case MATCH_BRANDING_RESOURCE_MAP_CACHE:
   2900                 tableToChange = TABLE_BRANDING_RESOURCE_MAP_CACHE;
   2901                 break;
   2902 
   2903             // mcs/rmq stuff
   2904             case MATCH_OUTGOING_RMQ_MESSAGES:
   2905                 tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
   2906                 break;
   2907 
   2908             case MATCH_LAST_RMQ_ID:
   2909                 tableToChange = TABLE_LAST_RMQ_ID;
   2910                 break;
   2911 
   2912             case MATCH_S2D_RMQ_IDS:
   2913                 tableToChange = TABLE_S2D_RMQ_IDS;
   2914                 break;
   2915 
   2916             default:
   2917                 throw new UnsupportedOperationException("Cannot delete that URL: " + url);
   2918         }
   2919 
   2920         if (idColumnName == null) {
   2921             idColumnName = "_id";
   2922         }
   2923 
   2924         if (changedItemId != null) {
   2925             appendWhere(whereClause, idColumnName, "=", changedItemId);
   2926         }
   2927 
   2928         if (DBG) log("delete from " + url + " WHERE  " + whereClause);
   2929 
   2930         int count = db.delete(tableToChange, whereClause.toString(), whereArgs);
   2931 
   2932         // see the comment at the declaration of tableToChange2 for an explanation
   2933         if (tableToChange2 != null){
   2934             count += db.delete(tableToChange2, whereClause.toString(), whereArgs);
   2935         }
   2936 
   2937         if (contactDeleted && count > 0) {
   2938             // since the contact cleanup triggers no longer work for cross database tables,
   2939             // we have to do it by hand here.
   2940             performContactRemovalCleanup(deletedContactId);
   2941         }
   2942 
   2943         if (count > 0) {
   2944             ContentResolver resolver = getContext().getContentResolver();
   2945 
   2946             // In most case, we query contacts with presence and chats joined, thus
   2947             // we should also notify that contacts changes when presence or chats changed.
   2948             if (match == MATCH_CHATS || match == MATCH_CHATS_ID
   2949                     || match == MATCH_PRESENCE || match == MATCH_PRESENCE_ID
   2950                     || match == MATCH_CONTACTS_BAREBONE) {
   2951                 resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
   2952             }
   2953 
   2954             if (notifyMessagesContentUri) {
   2955                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2956             }
   2957 
   2958             if (notifyMessagesByContactContentUri) {
   2959                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2960                 resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
   2961             }
   2962 
   2963             if (notifyMessagesByThreadIdContentUri) {
   2964                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   2965                 resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
   2966             }
   2967 
   2968             if (notifyContactListContentUri) {
   2969                 resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
   2970             }
   2971 
   2972             if (notifyProviderAccountContentUri) {
   2973                 if (DBG) log("notify delete for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
   2974                 resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
   2975             }
   2976 
   2977             if (backfillQuickSwitchSlots) {
   2978                 backfillQuickSwitchSlots();
   2979             }
   2980         }
   2981 
   2982         return count;
   2983     }
   2984 
   2985     private int updateInternal(Uri url, ContentValues values, String userWhere,
   2986             String[] whereArgs) {
   2987         String tableToChange;
   2988         String idColumnName = null;
   2989         String changedItemId = null;
   2990         String accountStr = null;
   2991         long account = 0;
   2992         String contact = null;
   2993         long threadId = 0;
   2994         int count;
   2995 
   2996         StringBuilder whereClause = new StringBuilder();
   2997         if(userWhere != null) {
   2998             whereClause.append(userWhere);
   2999         }
   3000 
   3001         boolean notifyMessagesContentUri = false;
   3002         boolean notifyMessagesByContactContentUri = false;
   3003         boolean notifyMessagesByThreadIdContentUri = false;
   3004         boolean notifyContactListContentUri = false;
   3005         boolean notifyProviderAccountContentUri = false;
   3006 
   3007         int match = mUrlMatcher.match(url);
   3008         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   3009 
   3010         switch (match) {
   3011             case MATCH_PROVIDERS_BY_ID:
   3012                 changedItemId = url.getPathSegments().get(1);
   3013                 // fall through
   3014             case MATCH_PROVIDERS:
   3015                 tableToChange = TABLE_PROVIDERS;
   3016                 break;
   3017 
   3018             case MATCH_ACCOUNTS_BY_ID:
   3019                 changedItemId = url.getPathSegments().get(1);
   3020                 // fall through
   3021             case MATCH_ACCOUNTS:
   3022                 tableToChange = TABLE_ACCOUNTS;
   3023                 notifyProviderAccountContentUri = true;
   3024                 break;
   3025 
   3026             case MATCH_ACCOUNT_STATUS:
   3027                 changedItemId = url.getPathSegments().get(1);
   3028                 // fall through
   3029             case MATCH_ACCOUNTS_STATUS:
   3030                 tableToChange = TABLE_ACCOUNT_STATUS;
   3031                 notifyProviderAccountContentUri = true;
   3032                 break;
   3033 
   3034             case MATCH_CONTACTS:
   3035             case MATCH_CONTACTS_BAREBONE:
   3036                 tableToChange = TABLE_CONTACTS;
   3037                 break;
   3038 
   3039             case MATCH_CONTACTS_BY_PROVIDER:
   3040                 tableToChange = TABLE_CONTACTS;
   3041                 changedItemId = url.getPathSegments().get(2);
   3042                 idColumnName = Imps.Contacts.ACCOUNT;
   3043                 break;
   3044 
   3045             case MATCH_CONTACT:
   3046                 tableToChange = TABLE_CONTACTS;
   3047                 changedItemId = url.getPathSegments().get(1);
   3048                 break;
   3049 
   3050             case MATCH_CONTACTS_BULK:
   3051                 count = updateBulkContacts(values, userWhere);
   3052                 // notify change using the "content://im/contacts" url,
   3053                 // so the change will be observed by listeners interested
   3054                 // in contacts changes.
   3055                 if (count > 0) {
   3056                     getContext().getContentResolver().notifyChange(
   3057                             Imps.Contacts.CONTENT_URI, null);
   3058                 }
   3059                 return count;
   3060 
   3061             case MATCH_CONTACTLIST:
   3062                 tableToChange = TABLE_CONTACT_LIST;
   3063                 changedItemId = url.getPathSegments().get(1);
   3064                 notifyContactListContentUri = true;
   3065                 break;
   3066 
   3067             case MATCH_CONTACTS_ETAGS:
   3068                 tableToChange = TABLE_CONTACTS_ETAG;
   3069                 break;
   3070 
   3071             case MATCH_CONTACTS_ETAG:
   3072                 tableToChange = TABLE_CONTACTS_ETAG;
   3073                 changedItemId = url.getPathSegments().get(1);
   3074                 break;
   3075 
   3076             case MATCH_MESSAGES:
   3077                 tableToChange = TABLE_MESSAGES;
   3078                 break;
   3079 
   3080             case MATCH_MESSAGES_BY_CONTACT:
   3081                 tableToChange = TABLE_MESSAGES;
   3082 
   3083                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   3084                 try {
   3085                     account = Long.parseLong(accountStr);
   3086                 } catch (NumberFormatException ex) {
   3087                     throw new IllegalArgumentException();
   3088                 }
   3089 
   3090                 contact = decodeURLSegment(url.getPathSegments().get(2));
   3091                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
   3092                         getContactId(db, accountStr, contact));
   3093 
   3094                 notifyMessagesContentUri = true;
   3095                 break;
   3096 
   3097             case MATCH_MESSAGES_BY_THREAD_ID:
   3098                 tableToChange = TABLE_MESSAGES;
   3099 
   3100                 try {
   3101                     threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
   3102                 } catch (NumberFormatException ex) {
   3103                     throw new IllegalArgumentException();
   3104                 }
   3105 
   3106                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
   3107 
   3108                 notifyMessagesContentUri = true;
   3109                 break;
   3110 
   3111             case MATCH_MESSAGE:
   3112                 tableToChange = TABLE_MESSAGES;
   3113                 changedItemId = url.getPathSegments().get(1);
   3114                 notifyMessagesContentUri = true;
   3115                 break;
   3116 
   3117             case MATCH_OTR_MESSAGES:
   3118                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   3119                 break;
   3120 
   3121             case MATCH_OTR_MESSAGES_BY_CONTACT:
   3122                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   3123 
   3124                 accountStr = decodeURLSegment(url.getPathSegments().get(1));
   3125                 try {
   3126                     account = Long.parseLong(accountStr);
   3127                 } catch (NumberFormatException ex) {
   3128                     throw new IllegalArgumentException();
   3129                 }
   3130 
   3131                 contact = decodeURLSegment(url.getPathSegments().get(2));
   3132                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=",
   3133                         getContactId(db, accountStr, contact));
   3134 
   3135                 notifyMessagesByContactContentUri = true;
   3136                 break;
   3137 
   3138             case MATCH_OTR_MESSAGES_BY_THREAD_ID:
   3139                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   3140 
   3141                 try {
   3142                     threadId = Long.parseLong(decodeURLSegment(url.getPathSegments().get(1)));
   3143                 } catch (NumberFormatException ex) {
   3144                     throw new IllegalArgumentException();
   3145                 }
   3146 
   3147                 appendWhere(whereClause, Imps.Messages.THREAD_ID, "=", threadId);
   3148 
   3149                 notifyMessagesByThreadIdContentUri = true;
   3150                 break;
   3151 
   3152             case MATCH_OTR_MESSAGE:
   3153                 tableToChange = TABLE_IN_MEMORY_MESSAGES;
   3154                 changedItemId = url.getPathSegments().get(1);
   3155                 notifyMessagesContentUri = true;
   3156                 break;
   3157 
   3158             case MATCH_AVATARS:
   3159                 tableToChange = TABLE_AVATARS;
   3160                 break;
   3161 
   3162             case MATCH_AVATAR:
   3163                 tableToChange = TABLE_AVATARS;
   3164                 changedItemId = url.getPathSegments().get(1);
   3165                 break;
   3166 
   3167             case MATCH_AVATAR_BY_PROVIDER:
   3168                 tableToChange = TABLE_AVATARS;
   3169                 changedItemId = url.getPathSegments().get(2);
   3170                 idColumnName = Imps.Avatars.ACCOUNT;
   3171                 break;
   3172 
   3173             case MATCH_CHATS:
   3174                 tableToChange = TABLE_CHATS;
   3175                 break;
   3176 
   3177             case MATCH_CHATS_ID:
   3178                 tableToChange = TABLE_CHATS;
   3179                 changedItemId = url.getPathSegments().get(1);
   3180                 idColumnName = Imps.Chats.CONTACT_ID;
   3181                 break;
   3182 
   3183             case MATCH_PRESENCE:
   3184                 //if (DBG) log("update presence: where='" + userWhere + "'");
   3185                 tableToChange = TABLE_PRESENCE;
   3186                 break;
   3187 
   3188             case MATCH_PRESENCE_ID:
   3189                 tableToChange = TABLE_PRESENCE;
   3190                 changedItemId = url.getPathSegments().get(1);
   3191                 idColumnName = Imps.Presence.CONTACT_ID;
   3192                 break;
   3193 
   3194             case MATCH_PRESENCE_BULK:
   3195                 count = updateBulkPresence(values, userWhere, whereArgs);
   3196                 // notify change using the "content://im/contacts" url,
   3197                 // so the change will be observed by listeners interested
   3198                 // in contacts changes.
   3199                 if (count > 0) {
   3200                      getContext().getContentResolver().notifyChange(Imps.Contacts.CONTENT_URI, null);
   3201                 }
   3202 
   3203                 return count;
   3204 
   3205             case MATCH_INVITATION:
   3206                 tableToChange = TABLE_INVITATIONS;
   3207                 changedItemId = url.getPathSegments().get(1);
   3208                 break;
   3209 
   3210             case MATCH_SESSIONS:
   3211                 tableToChange = TABLE_SESSION_COOKIES;
   3212                 break;
   3213 
   3214             case MATCH_PROVIDER_SETTINGS_BY_ID_AND_NAME:
   3215                 tableToChange = TABLE_PROVIDER_SETTINGS;
   3216 
   3217                 String providerId = url.getPathSegments().get(1);
   3218                 String name = url.getPathSegments().get(2);
   3219 
   3220                 if (values.containsKey(Imps.ProviderSettings.PROVIDER) ||
   3221                         values.containsKey(Imps.ProviderSettings.NAME)) {
   3222                     throw new SecurityException("Cannot override the value for provider|name");
   3223                 }
   3224 
   3225                 appendWhere(whereClause, Imps.ProviderSettings.PROVIDER, "=", providerId);
   3226                 appendWhere(whereClause, Imps.ProviderSettings.NAME, "=", name);
   3227 
   3228                 break;
   3229 
   3230             case MATCH_OUTGOING_RMQ_MESSAGES:
   3231                 tableToChange = TABLE_OUTGOING_RMQ_MESSAGES;
   3232                 break;
   3233 
   3234             case MATCH_LAST_RMQ_ID:
   3235                 tableToChange = TABLE_LAST_RMQ_ID;
   3236                 break;
   3237 
   3238             case MATCH_S2D_RMQ_IDS:
   3239                 tableToChange = TABLE_S2D_RMQ_IDS;
   3240                 break;
   3241 
   3242             default:
   3243                 throw new UnsupportedOperationException("Cannot update URL: " + url);
   3244         }
   3245 
   3246         if (idColumnName == null) {
   3247             idColumnName = "_id";
   3248         }
   3249         if(changedItemId != null) {
   3250             appendWhere(whereClause, idColumnName, "=", changedItemId);
   3251         }
   3252 
   3253         if (DBG) log("update " + url + " WHERE " + whereClause);
   3254 
   3255         count = db.update(tableToChange, values, whereClause.toString(), whereArgs);
   3256 
   3257         if (count > 0) {
   3258             ContentResolver resolver = getContext().getContentResolver();
   3259 
   3260             // In most case, we query contacts with presence and chats joined, thus
   3261             // we should also notify that contacts changes when presence or chats changed.
   3262             if (match == MATCH_CHATS || match == MATCH_CHATS_ID
   3263                     || match == MATCH_PRESENCE || match == MATCH_PRESENCE_ID
   3264                     || match == MATCH_CONTACTS_BAREBONE) {
   3265                 resolver.notifyChange(Imps.Contacts.CONTENT_URI, null);
   3266             }
   3267 
   3268             if (notifyMessagesContentUri) {
   3269                 if (DBG) log("notify change for " + Imps.Messages.CONTENT_URI);
   3270                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   3271             }
   3272 
   3273             if (notifyMessagesByContactContentUri) {
   3274                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   3275                 resolver.notifyChange(Imps.Messages.getContentUriByContact(account, contact), null);
   3276             }
   3277 
   3278             if (notifyMessagesByThreadIdContentUri) {
   3279                 resolver.notifyChange(Imps.Messages.CONTENT_URI, null);
   3280                 resolver.notifyChange(Imps.Messages.getContentUriByThreadId(threadId), null);
   3281             }
   3282 
   3283             if (notifyContactListContentUri) {
   3284                 resolver.notifyChange(Imps.ContactList.CONTENT_URI, null);
   3285             }
   3286 
   3287             if (notifyProviderAccountContentUri) {
   3288                 if (DBG) log("notify change for " + Imps.Provider.CONTENT_URI_WITH_ACCOUNT);
   3289                 resolver.notifyChange(Imps.Provider.CONTENT_URI_WITH_ACCOUNT, null);
   3290             }
   3291         }
   3292 
   3293         return count;
   3294     }
   3295 
   3296     @Override
   3297     public ParcelFileDescriptor openFile(Uri uri, String mode)
   3298             throws FileNotFoundException {
   3299         return openFileHelper(uri, mode);
   3300     }
   3301 
   3302     private static void appendWhere(StringBuilder where, String columnName,
   3303             String condition, Object value) {
   3304         if (where.length() > 0) {
   3305             where.append(" AND ");
   3306         }
   3307         where.append(columnName).append(condition);
   3308         if(value != null) {
   3309             DatabaseUtils.appendValueToSql(where, value);
   3310         }
   3311     }
   3312 
   3313     private static void appendWhere(StringBuilder where, String clause) {
   3314         if (where.length() > 0) {
   3315             where.append(" AND ");
   3316         }
   3317         where.append(clause);
   3318     }
   3319 
   3320     private static String decodeURLSegment(String segment) {
   3321         try {
   3322             return URLDecoder.decode(segment, "UTF-8");
   3323         } catch (UnsupportedEncodingException e) {
   3324             // impossible
   3325             return segment;
   3326         }
   3327     }
   3328 
   3329     static void log(String message) {
   3330         Log.d(LOG_TAG, message);
   3331     }
   3332 }
   3333