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