Merge "VoicemailStatus content provider implementation."
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..096edf6
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <!-- Maximum size of photos inserted in social stream items -->
+    <integer name="config_stream_item_photo_max_bytes">71680</integer>
+
+</resources>
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 69cf2b4..a8d4752 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -64,6 +64,8 @@
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
 import android.provider.SocialContract.Activities;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Voicemails;
@@ -98,7 +100,7 @@
      *   600-699 Ice Cream Sandwich
      * </pre>
      */
-    static final int DATABASE_VERSION = 604;
+    static final int DATABASE_VERSION = 605;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -106,6 +108,8 @@
     public interface Tables {
         public static final String CONTACTS = "contacts";
         public static final String RAW_CONTACTS = "raw_contacts";
+        public static final String STREAM_ITEMS = "stream_items";
+        public static final String STREAM_ITEM_PHOTOS = "stream_item_photos";
         public static final String PACKAGES = "packages";
         public static final String MIMETYPES = "mimetypes";
         public static final String PHONE_LOOKUP = "phone_lookup";
@@ -498,6 +502,29 @@
         String CONCRETE_STATUS_ICON = ALIAS + "." + StatusUpdates.STATUS_ICON;
     }
 
+    public interface StreamItemsColumns {
+        String CONCRETE_ID = Tables.STREAM_ITEMS + "." + BaseColumns._ID;
+        String CONCRETE_RAW_CONTACT_ID = Tables.STREAM_ITEMS + "." + StreamItems.RAW_CONTACT_ID;
+        String CONCRETE_PACKAGE = Tables.STREAM_ITEMS + "." + StreamItems.RES_PACKAGE;
+        String CONCRETE_ICON = Tables.STREAM_ITEMS + "." + StreamItems.RES_ICON;
+        String CONCRETE_LABEL = Tables.STREAM_ITEMS + "." + StreamItems.RES_LABEL;
+        String CONCRETE_TEXT = Tables.STREAM_ITEMS + "." + StreamItems.TEXT;
+        String CONCRETE_TIMESTAMP = Tables.STREAM_ITEMS + "." + StreamItems.TIMESTAMP;
+        String CONCRETE_COMMENTS = Tables.STREAM_ITEMS + "." + StreamItems.COMMENTS;
+        String CONCRETE_ACTION = Tables.STREAM_ITEMS + "." + StreamItems.ACTION;
+        String CONCRETE_ACTION_URI = Tables.STREAM_ITEMS + "." + StreamItems.ACTION_URI;
+    }
+
+    public interface StreamItemPhotosColumns {
+        String CONCRETE_ID = Tables.STREAM_ITEM_PHOTOS + "." + BaseColumns._ID;
+        String CONCRETE_STREAM_ITEM_ID = Tables.STREAM_ITEM_PHOTOS + "."
+                + StreamItemPhotos.STREAM_ITEM_ID;
+        String CONCRETE_SORT_INDEX = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SORT_INDEX;
+        String CONCRETE_PICTURE = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.PICTURE;
+        String CONCRETE_ACTION = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.ACTION;
+        String CONCRETE_ACTION_URI = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.ACTION_URI;
+    }
+
     public interface PropertiesColumns {
         String PROPERTY_KEY = "property_key";
         String PROPERTY_VALUE = "property_value";
@@ -873,6 +900,30 @@
                 RawContacts.ACCOUNT_NAME +
         ");");
 
+        db.execSQL("CREATE TABLE " + Tables.STREAM_ITEMS + " (" +
+                StreamItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                StreamItems.RAW_CONTACT_ID + " INTEGER NOT NULL, " +
+                StreamItems.RES_PACKAGE + " INTEGER NOT NULL, " +
+                StreamItems.RES_ICON + " INTEGER, " +
+                StreamItems.RES_LABEL + " INTEGER, " +
+                StreamItems.TEXT + " TEXT NOT NULL, " +
+                StreamItems.TIMESTAMP + " INTEGER NOT NULL, " +
+                StreamItems.COMMENTS + " TEXT NOT NULL, " +
+                StreamItems.ACTION + " TEXT, " +
+                StreamItems.ACTION_URI + " TEXT, " +
+                "FOREIGN KEY(" + StreamItems.RAW_CONTACT_ID + ") REFERENCES " +
+                        Tables.RAW_CONTACTS + "(" + RawContacts._ID + "));");
+
+        db.execSQL("CREATE TABLE " + Tables.STREAM_ITEM_PHOTOS + " (" +
+                StreamItemPhotos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                StreamItemPhotos.STREAM_ITEM_ID + " INTEGER NOT NULL, " +
+                StreamItemPhotos.SORT_INDEX + " INTEGER, " +
+                StreamItemPhotos.PICTURE + " BLOB, " +
+                StreamItemPhotos.ACTION + " TEXT, " +
+                StreamItemPhotos.ACTION_URI + " TEXT, " +
+                "FOREIGN KEY(" + StreamItemPhotos.STREAM_ITEM_ID + ") REFERENCES " +
+                        Tables.STREAM_ITEMS + "(" + StreamItems._ID + "));");
+
         // TODO readd the index and investigate a controlled use of it
 //        db.execSQL("CREATE INDEX raw_contacts_agg_index ON " + Tables.RAW_CONTACTS + " (" +
 //                RawContactsColumns.AGGREGATION_NEEDED +
@@ -1987,6 +2038,11 @@
             oldVersion = 604;
         }
 
+        if (oldVersion < 605) {
+            upgradeToVersion605(db);
+            oldVersion = 605;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -3073,6 +3129,30 @@
         ");");
     }
 
+    private void upgradeToVersion605(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE stream_items(" +
+                "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                "raw_contact_id INTEGER NOT NULL, " +
+                "package_id INTEGER NOT NULL, " +
+                "icon INTEGER, " +
+                "label INTEGER, " +
+                "text TEXT NOT NULL, " +
+                "timestamp INTEGER NOT NULL, " +
+                "comments TEXT NOT NULL, " +
+                "action TEXT, " +
+                "action_uri TEXT, " +
+                "FOREIGN KEY(raw_contact_id) REFERENCES raw_contacts(_id));");
+
+        db.execSQL("CREATE TABLE stream_item_photos(" +
+                "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                "stream_item_id INTEGER NOT NULL, " +
+                "sort_index INTEGER, " +
+                "picture BLOB, " +
+                "action TEXT, " +
+                "action_uri TEXT, " +
+                "FOREIGN KEY(stream_item_id) REFERENCES stream_items(_id));");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -3206,6 +3286,8 @@
 
         db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
         db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
+        db.execSQL("DELETE FROM " + Tables.STREAM_ITEMS + ";");
+        db.execSQL("DELETE FROM " + Tables.STREAM_ITEM_PHOTOS + ";");
         db.execSQL("DELETE FROM " + Tables.DATA + ";");
         db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";");
         db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";");
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 4a61410..6a409b4 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -39,6 +39,8 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.util.DbQueryUtils;
 import com.android.vcard.VCardComposer;
@@ -67,6 +69,7 @@
 import android.content.SyncAdapterType;
 import android.content.UriMatcher;
 import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
 import android.database.CrossProcessCursor;
 import android.database.Cursor;
 import android.database.CursorWindow;
@@ -118,6 +121,8 @@
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
 import android.provider.LiveFolders;
 import android.provider.OpenableColumns;
 import android.provider.SyncStateContract;
@@ -170,6 +175,9 @@
     /** Default for the maximum number of returned aggregation suggestions. */
     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
 
+    /** Limit for the maximum number of social stream items to store under a raw contact. */
+    private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5;
+
     /**
      * Property key for the legacy contact import version. The need for a version
      * as opposed to a boolean flag is that if we discover bugs in the contact import process,
@@ -232,11 +240,15 @@
     private static final int CONTACTS_ID_ENTITIES = 1014;
     private static final int CONTACTS_LOOKUP_ENTITIES = 1015;
     private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1016;
+    private static final int CONTACTS_ID_STREAM_ITEMS = 1017;
+    private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1018;
+    private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1019;
 
     private static final int RAW_CONTACTS = 2002;
     private static final int RAW_CONTACTS_ID = 2003;
     private static final int RAW_CONTACTS_DATA = 2004;
     private static final int RAW_CONTACT_ENTITY_ID = 2005;
+    private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2006;
 
     private static final int DATA = 3000;
     private static final int DATA_ID = 3001;
@@ -298,6 +310,13 @@
 
     private static final int DATA_USAGE_FEEDBACK_ID = 20001;
 
+    private static final int STREAM_ITEMS = 21000;
+    private static final int STREAM_ITEMS_PHOTOS = 21001;
+    private static final int STREAM_ITEMS_ID = 21002;
+    private static final int STREAM_ITEMS_ID_PHOTOS = 21003;
+    private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004;
+    private static final int STREAM_ITEMS_LIMIT = 21005;
+
     private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
             RawContactsColumns.CONCRETE_ID + "=? AND "
                     + GroupsColumns.CONCRETE_ACCOUNT_NAME
@@ -833,6 +852,31 @@
             .add(StatusUpdates.STATUS_LABEL)
             .build();
 
+    /** Contains StreamItems columns */
+    private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder()
+            .add(StreamItems._ID, StreamItemsColumns.CONCRETE_ID)
+            .add(RawContacts.CONTACT_ID)
+            .add(StreamItems.RAW_CONTACT_ID)
+            .add(StreamItems.RES_PACKAGE)
+            .add(StreamItems.RES_ICON)
+            .add(StreamItems.RES_LABEL)
+            .add(StreamItems.TEXT)
+            .add(StreamItems.TIMESTAMP)
+            .add(StreamItems.COMMENTS)
+            .add(StreamItems.ACTION)
+            .add(StreamItems.ACTION_URI)
+            .build();
+
+    private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder()
+            .add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID)
+            .add(StreamItems.RAW_CONTACT_ID)
+            .add(StreamItemPhotos.STREAM_ITEM_ID)
+            .add(StreamItemPhotos.SORT_INDEX)
+            .add(StreamItemPhotos.PICTURE)
+            .add(StreamItemPhotos.ACTION, StreamItemPhotosColumns.CONCRETE_ACTION)
+            .add(StreamItemPhotos.ACTION_URI, StreamItemPhotosColumns.CONCRETE_ACTION_URI)
+            .build();
+
     /** Contains Live Folders columns */
     private static final ProjectionMap sLiveFoldersProjectionMap = ProjectionMap.builder()
             .add(LiveFolders._ID, Contacts._ID)
@@ -902,6 +946,8 @@
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
                 AGGREGATION_SUGGESTIONS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items",
+                CONTACTS_ID_STREAM_ITEMS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
@@ -913,6 +959,10 @@
                 CONTACTS_LOOKUP_ENTITIES);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
                 CONTACTS_LOOKUP_ID_ENTITIES);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items",
+                CONTACTS_LOOKUP_STREAM_ITEMS);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items",
+                CONTACTS_LOOKUP_ID_STREAM_ITEMS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
                 CONTACTS_AS_MULTI_VCARD);
@@ -925,6 +975,8 @@
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items",
+                RAW_CONTACTS_ID_STREAM_ITEMS);
 
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
 
@@ -1000,6 +1052,14 @@
         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity",
                 PROFILE_RAW_CONTACTS_ID_ENTITIES);
 
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS);
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS);
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS);
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#",
+                STREAM_ITEMS_ID_PHOTOS_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT);
+
         HashMap<String, Integer> tmpTypeMap = new HashMap<String, Integer>();
         tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_CALL, DataUsageStatColumns.USAGE_TYPE_INT_CALL);
         tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_LONG_TEXT,
@@ -1085,6 +1145,9 @@
 
     private ProfileIdCache mProfileIdCache;
 
+    /** Limit for the maximum byte size of social stream item photos (loaded from config.xml). */
+    private int mMaxStreamItemPhotoSizeBytes;
+
     private HashMap<String, DataRowHandler> mDataRowHandlers;
     private ContactsDatabaseHelper mDbHelper;
 
@@ -1138,6 +1201,10 @@
         StrictMode.setThreadPolicy(
                 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
 
+        Resources resources = getContext().getResources();
+        mMaxStreamItemPhotoSizeBytes = resources.getInteger(
+                R.integer.config_stream_item_photo_max_bytes);
+
         mProfileIdCache = new ProfileIdCache();
         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
         mContactDirectoryManager = new ContactDirectoryManager(this);
@@ -1812,6 +1879,13 @@
                 break;
             }
 
+            case RAW_CONTACTS_ID_STREAM_ITEMS: {
+                values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1));
+                id = insertStreamItem(uri, values);
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                break;
+            }
+
             case PROFILE_RAW_CONTACTS: {
                 enforceProfilePermission(true);
                 id = insertRawContact(uri, values, callerIsSyncAdapter, true);
@@ -1842,6 +1916,25 @@
                 break;
             }
 
+            case STREAM_ITEMS: {
+                id = insertStreamItem(uri, values);
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                break;
+            }
+
+            case STREAM_ITEMS_PHOTOS: {
+                id = insertStreamItemPhoto(uri, values);
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                break;
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS: {
+                values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1));
+                id = insertStreamItemPhoto(uri, values);
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                break;
+            }
+
             default:
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.insert(uri, values);
@@ -2077,6 +2170,238 @@
         return id;
     }
 
+    /**
+     * Inserts an item in the stream_items table.  The account is checked against the
+     * account in the raw contact for which the stream item is being inserted.  If the
+     * new stream item results in more stream items under this raw contact than the limit,
+     * the oldest one will be deleted (note that if the stream item inserted was the
+     * oldest, it will be immediately deleted, and this will return 0).
+     *
+     * @param uri the insertion URI
+     * @param values the values for the new row
+     * @return the stream item _ID of the newly created row, or 0 if it was not created
+     */
+    private long insertStreamItem(Uri uri, ContentValues values) {
+        long id = 0;
+        mValues.clear();
+        mValues.putAll(values);
+
+        long rawContactId = mValues.getAsLong(StreamItems.RAW_CONTACT_ID);
+
+        // If the data being inserted belongs to the user's profile entry, check for the
+        // WRITE_PROFILE permission before proceeding.
+        enforceProfilePermissionForRawContact(rawContactId, true);
+
+        // Ensure that the raw contact exists and belongs to the caller's account.
+        Account account = resolveAccount(uri, mValues);
+        enforceModifyingAccount(account, rawContactId);
+
+        // Insert the new stream item.
+        id = mDb.insert(Tables.STREAM_ITEMS, null, values);
+
+        // Check to see if we're over the limit for stream items under this raw contact.
+        // It's possible that the inserted stream item is older than the the existing
+        // ones, in which case it may be deleted immediately (resetting the ID to 0).
+        id = cleanUpOldStreamItems(rawContactId, id);
+
+        return id;
+    }
+
+    /**
+     * Inserts an item in the stream_item_photos table.  The account is checked against
+     * the account in the raw contact that owns the stream item being modified.
+     *
+     * @param uri the insertion URI
+     * @param values the values for the new row
+     * @return the stream item photo _ID of the newly created row
+     */
+    private long insertStreamItemPhoto(Uri uri, ContentValues values) {
+        long id = 0;
+        mValues.clear();
+        mValues.putAll(values);
+
+        long streamItemId = mValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID);
+        if (streamItemId != 0) {
+            long rawContactId = lookupRawContactIdForStreamId(streamItemId);
+
+            // If the data being inserted belongs to the user's profile entry, check for the
+            // WRITE_PROFILE permission before proceeding.
+            enforceProfilePermissionForRawContact(rawContactId, true);
+
+            // Ensure that the raw contact exists and belongs to the caller's account.
+            Account account = resolveAccount(uri, mValues);
+            enforceModifyingAccount(account, rawContactId);
+
+            // Make certain that the photo doesn't exceed our maximum byte size.
+            byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PICTURE);
+            Log.i(TAG, "Inserting " + photoBytes.length + "-byte photo (max allowed " +
+                    mMaxStreamItemPhotoSizeBytes + " bytes)");
+            if (photoBytes.length > mMaxStreamItemPhotoSizeBytes) {
+                throw new IllegalArgumentException("Stream item photos cannot be more than " +
+                    mMaxStreamItemPhotoSizeBytes + " bytes (received picture with " +
+                        photoBytes.length + " bytes)");
+            }
+
+            id = mDb.insert(Tables.STREAM_ITEM_PHOTOS, null, values);
+        }
+        return id;
+    }
+
+    /**
+     * Looks up the raw contact ID that owns the specified stream item.
+     * @param streamItemId The ID of the stream item.
+     * @return The associated raw contact ID, or -1 if no such stream item exists.
+     */
+    private long lookupRawContactIdForStreamId(long streamItemId) {
+        long rawContactId = -1;
+        Cursor c = mDb.query(Tables.STREAM_ITEMS, new String[]{StreamItems.RAW_CONTACT_ID},
+                StreamItems._ID + "=?", new String[]{String.valueOf(streamItemId)},
+                null, null, null);
+        try {
+            if (c.moveToFirst()) {
+                rawContactId = c.getLong(0);
+            }
+        } finally {
+            c.close();
+        }
+        return rawContactId;
+    }
+
+    /**
+     * Checks whether the given raw contact ID is owned by the given account.
+     * If the resolved account is null, this will return true iff the raw contact
+     * is also associated with the "null" account.
+     *
+     * If the resolved account does not match, this will throw a security exception.
+     * @param account The resolved account (may be null).
+     * @param rawContactId The raw contact ID to check for.
+     */
+    private void enforceModifyingAccount(Account account, long rawContactId) {
+        String accountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
+                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + "=? AND "
+                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "=?";
+        String noAccountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
+                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL AND "
+                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL";
+        Cursor c;
+        if (account != null) {
+            c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContactsColumns.CONCRETE_ID},
+                    accountSelection,
+                    new String[]{String.valueOf(rawContactId), mAccount.name, mAccount.type},
+                    null, null, null);
+        } else {
+            c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContactsColumns.CONCRETE_ID},
+                    noAccountSelection, new String[]{String.valueOf(rawContactId)},
+                    null, null, null);
+        }
+        try {
+            if(c.getCount() == 0) {
+                throw new SecurityException("Caller account does not match raw contact ID "
+                    + rawContactId);
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Checks whether the given selection of stream items matches up with the given
+     * account.  If any of the raw contacts fail the account check, this will throw a
+     * security exception.
+     * @param account The resolved account (may be null).
+     * @param selection The selection.
+     * @param selectionArgs The selection arguments.
+     * @return The list of stream item IDs that would be included in this selection.
+     */
+    private List<Long> enforceModifyingAccountForStreamItems(Account account, String selection,
+            String[] selectionArgs) {
+        List<Long> streamItemIds = Lists.newArrayList();
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        setTablesAndProjectionMapForStreamItems(qb);
+        Cursor c = qb.query(mDb,
+                new String[]{StreamItems._ID, StreamItems.RAW_CONTACT_ID},
+                selection, selectionArgs, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                streamItemIds.add(c.getLong(0));
+
+                // Throw a security exception if the account doesn't match the raw contact's.
+                enforceModifyingAccount(account, c.getLong(1));
+            }
+        } finally {
+            c.close();
+        }
+        return streamItemIds;
+    }
+
+    /**
+     * Checks whether the given selection of stream item photos matches up with the given
+     * account.  If any of the raw contacts fail the account check, this will throw a
+     * security exception.
+     * @param account The resolved account (may be null).
+     * @param selection The selection.
+     * @param selectionArgs The selection arguments.
+     * @return The list of stream item photo IDs that would be included in this selection.
+     */
+    private List<Long> enforceModifyingAccountForStreamItemPhotos(Account account, String selection,
+            String[] selectionArgs) {
+        List<Long> streamItemPhotoIds = Lists.newArrayList();
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        setTablesAndProjectionMapForStreamItemPhotos(qb);
+        Cursor c = qb.query(mDb, new String[]{StreamItemPhotos._ID, StreamItems.RAW_CONTACT_ID},
+                selection, selectionArgs, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                streamItemPhotoIds.add(c.getLong(0));
+
+                // Throw a security exception if the account doesn't match the raw contact's.
+                enforceModifyingAccount(account, c.getLong(1));
+            }
+        } finally {
+            c.close();
+        }
+        return streamItemPhotoIds;
+    }
+
+    /**
+     * Queries the database for stream items under the given raw contact.  If there are
+     * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT},
+     * the oldest entries (as determined by timestamp) will be deleted.
+     * @param rawContactId The raw contact ID to examine for stream items.
+     * @param insertedStreamItemId The ID of the stream item that was just inserted,
+     *     prompting this cleanup.  Callers may pass 0 if no insertion prompted the
+     *     cleanup.
+     * @return The ID of the inserted stream item if it still exists after cleanup;
+     *     0 otherwise.
+     */
+    private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) {
+        long postCleanupInsertedStreamId = insertedStreamItemId;
+        Cursor c = mDb.query(Tables.STREAM_ITEMS, new String[]{StreamItems._ID},
+                StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
+                null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC");
+        try {
+            int streamItemCount = c.getCount();
+            if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
+                // Still under the limit - nothing to clean up!
+                return insertedStreamItemId;
+            } else {
+                c.moveToLast();
+                while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
+                    long streamItemId = c.getLong(0);
+                    if (insertedStreamItemId == streamItemId) {
+                        // The stream item just inserted is being deleted.
+                        postCleanupInsertedStreamId = 0;
+                    }
+                    deleteStreamItem(c.getLong(0));
+                    c.moveToPrevious();
+                }
+            }
+        } finally {
+            c.close();
+        }
+        return postCleanupInsertedStreamId;
+    }
+
     public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
         mDbHelper.updateRawContactDisplayName(db, rawContactId);
     }
@@ -2529,6 +2854,33 @@
                 return deleteStatusUpdates(selection, selectionArgs);
             }
 
+            case STREAM_ITEMS: {
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                return deleteStreamItems(uri, new ContentValues(), selection, selectionArgs);
+            }
+
+            case STREAM_ITEMS_ID: {
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                return deleteStreamItems(uri, new ContentValues(),
+                        StreamItemsColumns.CONCRETE_ID + "=?",
+                        new String[]{uri.getLastPathSegment()});
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS: {
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                return deleteStreamItemPhotos(uri, new ContentValues(), selection, selectionArgs);
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS_ID: {
+                mSyncToNetwork |= !callerIsSyncAdapter;
+                String streamItemId = uri.getPathSegments().get(1);
+                String streamItemPhotoId = uri.getPathSegments().get(3);
+                return deleteStreamItemPhotos(uri, new ContentValues(),
+                        StreamItemPhotosColumns.CONCRETE_ID + "=? AND "
+                                + StreamItemPhotos.STREAM_ITEM_ID + "=?",
+                        new String[]{streamItemPhotoId, streamItemId});
+            }
+
             default: {
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
@@ -2611,6 +2963,47 @@
       return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
     }
 
+    private int deleteStreamItems(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        // First query for the stream items to be deleted, and check that they belong
+        // to the account.
+        Account account = resolveAccount(uri, values);
+        List<Long> streamItemIds = enforceModifyingAccountForStreamItems(
+                account, selection, selectionArgs);
+
+        // If no security exception has been thrown, we're fine to delete.
+        for (long streamItemId : streamItemIds) {
+            deleteStreamItem(streamItemId);
+        }
+
+        mVisibleTouched = true;
+        return streamItemIds.size();
+    }
+
+    private int deleteStreamItem(long streamItemId) {
+        // Note that this does not enforce the modifying account.
+        deleteStreamItemPhotos(streamItemId);
+        return mDb.delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?",
+                new String[]{String.valueOf(streamItemId)});
+    }
+
+    private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        // First query for the stream item photos to be deleted, and check that they
+        // belong to the account.
+        Account account = resolveAccount(uri, values);
+        enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
+
+        // If no security exception has been thrown, we're fine to delete.
+        return mDb.delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs);
+    }
+
+    private int deleteStreamItemPhotos(long streamItemId) {
+        // Note that this does not enforce the modifying account.
+        return mDb.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID + "=?",
+                new String[]{String.valueOf(streamItemId)});
+    }
+
     private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
         mSyncToNetwork = true;
 
@@ -2782,6 +3175,39 @@
                 break;
             }
 
+            case STREAM_ITEMS: {
+                count = updateStreamItems(uri, values, selection, selectionArgs);
+                break;
+            }
+
+            case STREAM_ITEMS_ID: {
+                count = updateStreamItems(uri, values, StreamItemsColumns.CONCRETE_ID + "=?",
+                        new String[]{uri.getLastPathSegment()});
+                break;
+            }
+
+            case STREAM_ITEMS_PHOTOS: {
+                count = updateStreamItemPhotos(uri, values, selection, selectionArgs);
+                break;
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS: {
+                String streamItemId = uri.getPathSegments().get(1);
+                count = updateStreamItemPhotos(uri, values,
+                        StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{streamItemId});
+                break;
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS_ID: {
+                String streamItemId = uri.getPathSegments().get(1);
+                String streamItemPhotoId = uri.getPathSegments().get(3);
+                count = updateStreamItemPhotos(uri, values,
+                        StreamItemPhotosColumns.CONCRETE_ID + "=? AND " +
+                                StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?",
+                        new String[]{streamItemPhotoId, streamItemId});
+                break;
+            }
+
             case DIRECTORIES: {
                 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid());
                 count = 1;
@@ -2830,6 +3256,32 @@
         return updateCount;
     }
 
+    private int updateStreamItems(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        // Stream items can't be moved to a new raw contact.
+        values.remove(StreamItems.RAW_CONTACT_ID);
+
+        // Check that the stream items being updated belong to the account.
+        Account account = resolveAccount(uri, values);
+        enforceModifyingAccountForStreamItems(account, selection, selectionArgs);
+
+        // If there's been no exception, the update should be fine.
+        return mDb.update(Tables.STREAM_ITEMS, values, selection, selectionArgs);
+    }
+
+    private int updateStreamItemPhotos(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        // Stream item photos can't be moved to a new stream item.
+        values.remove(StreamItemPhotos.STREAM_ITEM_ID);
+
+        // Check that the stream item photos being updated belong to the account.
+        Account account = resolveAccount(uri, values);
+        enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
+
+        // If there's been no exception, the update should be fine.
+        return mDb.update(Tables.STREAM_ITEM_PHOTOS, values, selection, selectionArgs);
+    }
+
     /**
      * Build a where clause to select the rows to be updated in status_updates table.
      */
@@ -3649,6 +4101,45 @@
                 break;
             }
 
+            case CONTACTS_ID_STREAM_ITEMS: {
+                long contactId = Long.parseLong(uri.getPathSegments().get(1));
+                enforceProfilePermissionForContact(contactId, false);
+                setTablesAndProjectionMapForStreamItems(qb);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(RawContactsColumns.CONCRETE_CONTACT_ID + "=?");
+                break;
+            }
+
+            case CONTACTS_LOOKUP_STREAM_ITEMS:
+            case CONTACTS_LOOKUP_ID_STREAM_ITEMS: {
+                List<String> pathSegments = uri.getPathSegments();
+                int segmentCount = pathSegments.size();
+                if (segmentCount < 4) {
+                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                            "Missing a lookup key", uri));
+                }
+                String lookupKey = pathSegments.get(2);
+                if (segmentCount == 5) {
+                    long contactId = Long.parseLong(pathSegments.get(3));
+                    enforceProfilePermissionForContact(contactId, false);
+                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
+                    setTablesAndProjectionMapForStreamItems(lookupQb);
+                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
+                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
+                            RawContacts.CONTACT_ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
+                    if (c != null) {
+                        return c;
+                    }
+                }
+
+                setTablesAndProjectionMapForStreamItems(qb);
+                long contactId = lookupContactIdByLookupKey(db, lookupKey);
+                enforceProfilePermissionForContact(contactId, false);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(RawContacts.CONTACT_ID + "=?");
+                break;
+            }
+
             case CONTACTS_AS_VCARD: {
                 // When reading as vCard always use restricted view
                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
@@ -3864,6 +4355,53 @@
                 break;
             }
 
+            case STREAM_ITEMS: {
+                setTablesAndProjectionMapForStreamItems(qb);
+                break;
+            }
+
+            case STREAM_ITEMS_ID: {
+                setTablesAndProjectionMapForStreamItems(qb);
+                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+                qb.appendWhere(StreamItemsColumns.CONCRETE_ID + "=?");
+                break;
+            }
+
+            case STREAM_ITEMS_LIMIT: {
+                MatrixCursor cursor = new MatrixCursor(
+                        new String[]{StreamItems.MAX_ITEMS, StreamItems.PHOTO_MAX_BYTES}, 1);
+                cursor.addRow(
+                        new Object[]{
+                                MAX_STREAM_ITEMS_PER_RAW_CONTACT,
+                                mMaxStreamItemPhotoSizeBytes
+                        });
+                return cursor;
+            }
+
+            case STREAM_ITEMS_PHOTOS: {
+                setTablesAndProjectionMapForStreamItemPhotos(qb);
+                break;
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS: {
+                setTablesAndProjectionMapForStreamItemPhotos(qb);
+                String streamItemId = uri.getPathSegments().get(1);
+                selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
+                qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?");
+                break;
+            }
+
+            case STREAM_ITEMS_ID_PHOTOS_ID: {
+                setTablesAndProjectionMapForStreamItemPhotos(qb);
+                String streamItemId = uri.getPathSegments().get(1);
+                String streamItemPhotoId = uri.getPathSegments().get(3);
+                selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId);
+                selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
+                qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " +
+                        StreamItemPhotosColumns.CONCRETE_ID + "=?");
+                break;
+            }
+
             case PHONES: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
@@ -4068,6 +4606,14 @@
                 break;
             }
 
+            case RAW_CONTACTS_ID_STREAM_ITEMS: {
+                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
+                enforceProfilePermissionForRawContact(rawContactId, false);
+                setTablesAndProjectionMapForStreamItems(qb);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+                qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=?");
+                break;
+            }
 
             case PROFILE_RAW_CONTACTS: {
                 enforceProfilePermission(false);
@@ -5071,6 +5617,27 @@
         qb.setProjectionMap(sStatusUpdatesProjectionMap);
     }
 
+    private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(Tables.STREAM_ITEMS).append(" JOIN ").append(Tables.RAW_CONTACTS)
+                .append(" ON ").append(StreamItemsColumns.CONCRETE_RAW_CONTACT_ID).append("=")
+                .append(RawContactsColumns.CONCRETE_ID)
+                .append(" JOIN ").append(Tables.CONTACTS)
+                .append(" ON ").append(RawContactsColumns.CONCRETE_CONTACT_ID).append("=")
+                .append(ContactsColumns.CONCRETE_ID);
+        qb.setTables(sb.toString());
+        qb.setProjectionMap(sStreamItemsProjectionMap);
+    }
+
+    private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(Tables.STREAM_ITEM_PHOTOS).append(" JOIN ").append(Tables.STREAM_ITEMS)
+                .append(" ON ").append(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID).append("=")
+                .append(StreamItemsColumns.CONCRETE_ID);
+        qb.setTables(sb.toString());
+        qb.setProjectionMap(sStreamItemPhotosProjectionMap);
+    }
+
     private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
             String[] projection) {
         StringBuilder sb = new StringBuilder();
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 5d0b6dc..38132e2 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -62,6 +62,7 @@
             SearchManager.SUGGEST_COLUMN_INTENT_DATA,
             SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
             SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
+            SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT,
     };
 
     private static final char SNIPPET_START_MATCH = '\u0001';
@@ -109,6 +110,7 @@
         String icon1;
         String icon2;
         String filter;
+        String lastAccessTime;
 
         @SuppressWarnings({"unchecked"})
         public ArrayList asList(String[] projection) {
@@ -132,6 +134,7 @@
                 list.add(buildUri());
                 list.add(lookupKey);
                 list.add(filter);
+                list.add(lastAccessTime);
             } else {
                 for (int i = 0; i < projection.length; i++) {
                     addColumnValue(list, projection[i]);
@@ -159,6 +162,8 @@
                 list.add(lookupKey);
             } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) {
                 list.add(filter);
+            } else if (SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT.equals(column)) {
+                list.add(lastAccessTime);
             } else {
                 throw new IllegalArgumentException("Invalid column name: " + column);
             }
@@ -280,7 +285,8 @@
                         + Contacts.LOOKUP_KEY + ", "
                         + Contacts.PHOTO_THUMBNAIL_URI + ", "
                         + Contacts.DISPLAY_NAME + ", "
-                        + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE);
+                        + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", "
+                        + Contacts.LAST_TIME_CONTACTED);
         if (haveFilter) {
             sb.append(", " + SearchSnippetColumns.SNIPPET);
         }
@@ -315,8 +321,9 @@
                 suggestion.photoUri = c.getString(2);
                 suggestion.text1 = c.getString(3);
                 suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
+                suggestion.lastAccessTime = c.getString(5);
                 if (haveFilter) {
-                    suggestion.text2 = shortenSnippet(c.getString(5));
+                    suggestion.text2 = shortenSnippet(c.getString(6));
                 }
                 cursor.addRow(suggestion.asList(projection));
             }
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index f277fa6..50cd50e 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -49,6 +49,8 @@
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
 import android.test.AndroidTestCase;
 import android.test.MoreAsserts;
 import android.test.mock.MockContentResolver;
@@ -421,6 +423,26 @@
         return resultUri;
     }
 
+    protected Uri insertStreamItem(long rawContactId, ContentValues values, Account account) {
+        return mResolver.insert(
+                maybeAddAccountQueryParameters(
+                        Uri.withAppendedPath(
+                                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                                RawContacts.StreamItems.CONTENT_DIRECTORY),
+                        account),
+                values);
+    }
+
+    protected Uri insertStreamItemPhoto(long streamItemId, ContentValues values, Account account) {
+        return mResolver.insert(
+                maybeAddAccountQueryParameters(
+                        Uri.withAppendedPath(
+                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                        account),
+                values);
+    }
+
     protected Uri insertImHandle(long rawContactId, int protocol, String customProtocol,
             String handle) {
         ContentValues values = new ContentValues();
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 5dc71d2..1865904 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -354,6 +354,11 @@
         public String getResourceName(int resid) throws NotFoundException {
             return String.valueOf(resid);
         }
+
+        @Override
+        public int getInteger(int id) throws NotFoundException {
+            return mRes.getInteger(id);
+        }
     }
 
     static String sCallingPackage = null;
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 4265df7..475a57d 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -58,6 +58,8 @@
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
 import android.provider.LiveFolders;
 import android.provider.OpenableColumns;
 import android.test.MoreAsserts;
@@ -67,7 +69,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.Collator;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -2775,6 +2779,603 @@
         assertCursorValues(c, values);
     }
 
+    // Stream item query test cases.
+
+    public void testQueryStreamItemsByRawContactId() {
+        long rawContactId = createRawContact(mAccount);
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, values, mAccount);
+        assertStoredValues(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                        RawContacts.StreamItems.CONTENT_DIRECTORY),
+                values);
+    }
+
+    public void testQueryStreamItemsByContactId() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, values, null);
+        assertStoredValues(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
+                        Contacts.StreamItems.CONTENT_DIRECTORY),
+                values);
+    }
+
+    public void testQueryStreamItemsByLookupKey() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        String lookupKey = queryLookupKey(contactId);
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, values, null);
+        assertStoredValues(
+                Uri.withAppendedPath(
+                        Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+                        Contacts.StreamItems.CONTENT_DIRECTORY),
+                values);
+    }
+
+    public void testQueryStreamItemsByLookupKeyAndContactId() {
+        long rawContactId = createRawContact();
+        long contactId = queryContactId(rawContactId);
+        String lookupKey = queryLookupKey(contactId);
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, values, null);
+        assertStoredValues(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(
+                                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey),
+                                contactId),
+                        Contacts.StreamItems.CONTENT_DIRECTORY),
+                values);
+    }
+
+    public void testQueryStreamItems() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, values, null);
+        assertStoredValues(StreamItems.CONTENT_URI, values);
+    }
+
+    public void testQueryStreamItemsWithSelection() {
+        long rawContactId = createRawContact();
+        ContentValues firstValues = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, firstValues, null);
+
+        ContentValues secondValues = buildGenericStreamItemValues();
+        secondValues.put(StreamItems.TEXT, "Goodbye world");
+        insertStreamItem(rawContactId, secondValues, null);
+
+        // Select only the first stream item.
+        assertStoredValues(StreamItems.CONTENT_URI, StreamItems.TEXT + "=?",
+                new String[]{"Hello world"}, firstValues);
+
+        // Select only the second stream item.
+        assertStoredValues(StreamItems.CONTENT_URI, StreamItems.TEXT + "=?",
+                new String[]{"Goodbye world"}, secondValues);
+    }
+
+    public void testQueryStreamItemById() {
+        long rawContactId = createRawContact();
+        ContentValues firstValues = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
+        long firstStreamItemId = ContentUris.parseId(resultUri);
+
+        ContentValues secondValues = buildGenericStreamItemValues();
+        secondValues.put(StreamItems.TEXT, "Goodbye world");
+        resultUri = insertStreamItem(rawContactId, secondValues, null);
+        long secondStreamItemId = ContentUris.parseId(resultUri);
+
+        // Select only the first stream item.
+        assertStoredValues(ContentUris.withAppendedId(StreamItems.CONTENT_URI, firstStreamItemId),
+                firstValues);
+
+        // Select only the second stream item.
+        assertStoredValues(ContentUris.withAppendedId(StreamItems.CONTENT_URI, secondStreamItemId),
+                secondValues);
+    }
+
+    // Stream item photo insertion + query test cases.
+
+    public void testQueryStreamItemPhotoWithSelection() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+
+        ContentValues photo1Values = buildGenericStreamItemPhotoValues(1);
+        insertStreamItemPhoto(streamItemId, photo1Values, null);
+        ContentValues photo2Values = buildGenericStreamItemPhotoValues(2);
+        insertStreamItemPhoto(streamItemId, photo2Values, null);
+
+        // Select only the first photo.
+        assertStoredValues(StreamItems.CONTENT_PHOTO_URI, StreamItemPhotos.SORT_INDEX + "=?",
+                new String[]{"1"}, photo1Values);
+    }
+
+    public void testQueryStreamItemPhotoByStreamItemId() {
+        long rawContactId = createRawContact();
+
+        // Insert a first stream item.
+        ContentValues firstValues = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
+        long firstStreamItemId = ContentUris.parseId(resultUri);
+
+        // Insert a second stream item.
+        ContentValues secondValues = buildGenericStreamItemValues();
+        resultUri = insertStreamItem(rawContactId, secondValues, null);
+        long secondStreamItemId = ContentUris.parseId(resultUri);
+
+        // Add a photo to the first stream item.
+        ContentValues photo1Values = buildGenericStreamItemPhotoValues(1);
+        insertStreamItemPhoto(firstStreamItemId, photo1Values, null);
+
+        // Add a photo to the second stream item.
+        ContentValues photo2Values = buildGenericStreamItemPhotoValues(1);
+        photo2Values.put(StreamItemPhotos.PICTURE, "Some other picture".getBytes());
+        insertStreamItemPhoto(secondStreamItemId, photo2Values, null);
+
+        // Select only the photos from the second stream item.
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(StreamItems.CONTENT_URI, secondStreamItemId),
+                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), photo2Values);
+    }
+
+    public void testQueryStreamItemPhotoByStreamItemPhotoId() {
+        long rawContactId = createRawContact();
+
+        // Insert a first stream item.
+        ContentValues firstValues = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
+        long firstStreamItemId = ContentUris.parseId(resultUri);
+
+        // Insert a second stream item.
+        ContentValues secondValues = buildGenericStreamItemValues();
+        resultUri = insertStreamItem(rawContactId, secondValues, null);
+        long secondStreamItemId = ContentUris.parseId(resultUri);
+
+        // Add a photo to the first stream item.
+        ContentValues photo1Values = buildGenericStreamItemPhotoValues(1);
+        resultUri = insertStreamItemPhoto(firstStreamItemId, photo1Values, null);
+        long firstPhotoId = ContentUris.parseId(resultUri);
+
+        // Add a photo to the second stream item.
+        ContentValues photo2Values = buildGenericStreamItemPhotoValues(1);
+        photo2Values.put(StreamItemPhotos.PICTURE, "Some other picture".getBytes());
+        resultUri = insertStreamItemPhoto(secondStreamItemId, photo2Values, null);
+        long secondPhotoId = ContentUris.parseId(resultUri);
+
+        // Select the first photo.
+        assertStoredValues(ContentUris.withAppendedId(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(StreamItems.CONTENT_URI, firstStreamItemId),
+                        StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                firstPhotoId),
+                photo1Values);
+
+        // Select the second photo.
+        assertStoredValues(ContentUris.withAppendedId(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(StreamItems.CONTENT_URI, secondStreamItemId),
+                        StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                secondPhotoId),
+                photo2Values);
+    }
+
+    // Stream item insertion test cases.
+
+    public void testInsertStreamItemIntoOtherAccount() {
+        long rawContactId = createRawContact(mAccount);
+        ContentValues values = buildGenericStreamItemValues();
+        try {
+            insertStreamItem(rawContactId, values, mAccountTwo);
+            fail("Stream insertion was allowed in another account's raw contact.");
+        } catch (SecurityException expected) {
+            // Trying to insert stream items into account one's raw contact is forbidden.
+        }
+    }
+
+    public void testInsertStreamItemInProfileRequiresWriteProfileAccess() {
+        long profileRawContactId = createBasicProfileContact(new ContentValues());
+
+        // With our (default) write profile permission, we should be able to insert a stream item.
+        ContentValues values = buildGenericStreamItemValues();
+        insertStreamItem(profileRawContactId, values, null);
+
+        // Now take away write profile permission.
+        mActor.removePermissions("android.permission.WRITE_PROFILE");
+
+        // Try inserting another stream item.
+        try {
+            insertStreamItem(profileRawContactId, values, null);
+            fail("Should require WRITE_PROFILE access to insert a stream item in the profile.");
+        } catch (SecurityException expected) {
+            // Trying to insert a stream item in the profile without WRITE_PROFILE permission
+            // should fail.
+        }
+    }
+
+    public void testInsertStreamItemWithContentValues() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
+        mResolver.insert(StreamItems.CONTENT_URI, values);
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.StreamItems.CONTENT_DIRECTORY), values);
+    }
+
+    public void testInsertStreamItemOverLimit() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
+
+        List<Long> streamItemIds = Lists.newArrayList();
+
+        // Insert MAX + 1 stream items.
+        long baseTime = System.currentTimeMillis();
+        for (int i = 0; i < 6; i++) {
+            values.put(StreamItems.TIMESTAMP, baseTime + i);
+            Uri resultUri = mResolver.insert(StreamItems.CONTENT_URI, values);
+            streamItemIds.add(ContentUris.parseId(resultUri));
+        }
+        Long doomedStreamItemId = streamItemIds.get(0);
+
+        // There should only be MAX items.  The oldest one should have been cleaned up.
+        Cursor c = mResolver.query(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                        RawContacts.StreamItems.CONTENT_DIRECTORY),
+                new String[]{StreamItems._ID}, null, null, null);
+        try {
+            while(c.moveToNext()) {
+                long streamItemId = c.getLong(0);
+                streamItemIds.remove(streamItemId);
+            }
+        } finally {
+            c.close();
+        }
+
+        assertEquals(1, streamItemIds.size());
+        assertEquals(doomedStreamItemId, streamItemIds.get(0));
+    }
+
+    public void testInsertStreamItemOlderThanOldestInLimit() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
+
+        // Insert MAX stream items.
+        long baseTime = System.currentTimeMillis();
+        for (int i = 0; i < 5; i++) {
+            values.put(StreamItems.TIMESTAMP, baseTime + i);
+            Uri resultUri = mResolver.insert(StreamItems.CONTENT_URI, values);
+            assertNotSame("Expected non-0 stream item ID to be inserted",
+                    0L, ContentUris.parseId(resultUri));
+        }
+
+        // Now try to insert a stream item that's older.  It should be deleted immediately
+        // and return an ID of 0.
+        values.put(StreamItems.TIMESTAMP, baseTime - 1);
+        Uri resultUri = mResolver.insert(StreamItems.CONTENT_URI, values);
+        assertEquals(0L, ContentUris.parseId(resultUri));
+    }
+
+    // Stream item photo insertion test cases.
+
+    public void testInsertOversizedPhoto() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+
+        // Add a huge photo to the stream item.
+        ContentValues photoValues = buildGenericStreamItemPhotoValues(1);
+        byte[] photoBytes = new byte[(70 * 1024) + 1];
+        photoValues.put(StreamItemPhotos.PICTURE, photoBytes);
+        try {
+            insertStreamItemPhoto(streamItemId, photoValues, null);
+            fail("Should have failed due to image size");
+        } catch (IllegalArgumentException expected) {
+            // Huzzah!
+        }
+    }
+
+    public void testInsertStreamItemsAndPhotosInBatch() throws Exception {
+        long rawContactId = createRawContact();
+        ContentValues streamItemValues = buildGenericStreamItemValues();
+        ContentValues streamItemPhotoValues = buildGenericStreamItemPhotoValues(0);
+
+        ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
+        ops.add(ContentProviderOperation.newInsert(
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                        RawContacts.StreamItems.CONTENT_DIRECTORY))
+                .withValues(streamItemValues).build());
+        for (int i = 0; i < 5; i++) {
+            streamItemPhotoValues.put(StreamItemPhotos.SORT_INDEX, i);
+            ops.add(ContentProviderOperation.newInsert(StreamItems.CONTENT_PHOTO_URI)
+                    .withValues(streamItemPhotoValues)
+                    .withValueBackReference(StreamItemPhotos.STREAM_ITEM_ID, 0)
+                    .build());
+        }
+        mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
+
+        // Check that all five photos were inserted under the raw contact.
+        Cursor c = mResolver.query(StreamItems.CONTENT_URI, new String[]{StreamItems._ID},
+                StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
+                null);
+        long streamItemId = 0;
+        try {
+            assertEquals(1, c.getCount());
+            c.moveToFirst();
+            streamItemId = c.getLong(0);
+        } finally {
+            c.close();
+        }
+
+        c = mResolver.query(Uri.withAppendedPath(
+                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), new String[]{StreamItemPhotos._ID},
+                null, null, null);
+        try {
+            assertEquals(5, c.getCount());
+        } finally {
+            c.close();
+        }
+    }
+
+    // Stream item update test cases.
+
+    public void testUpdateStreamItemById() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+        values.put(StreamItems.TEXT, "Goodbye world");
+        mResolver.update(ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId), values,
+                null, null);
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.StreamItems.CONTENT_DIRECTORY), values);
+    }
+
+    public void testUpdateStreamItemWithContentValues() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+        values.put(StreamItems._ID, streamItemId);
+        values.put(StreamItems.TEXT, "Goodbye world");
+        mResolver.update(StreamItems.CONTENT_URI, values, null, null);
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.StreamItems.CONTENT_DIRECTORY), values);
+    }
+
+    public void testUpdateStreamItemFromOtherAccount() {
+        long rawContactId = createRawContact(mAccount);
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, mAccount);
+        long streamItemId = ContentUris.parseId(resultUri);
+        values.put(StreamItems._ID, streamItemId);
+        values.put(StreamItems.TEXT, "Goodbye world");
+        try {
+            mResolver.update(maybeAddAccountQueryParameters(StreamItems.CONTENT_URI, mAccountTwo),
+                    values, null, null);
+            fail("Should not be able to update stream items inserted by another account");
+        } catch (SecurityException expected) {
+            // Can't update the stream items from another account.
+        }
+    }
+
+    // Stream item photo update test cases.
+
+    public void testUpdateStreamItemPhotoById() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+        ContentValues photoValues = buildGenericStreamItemPhotoValues(1);
+        resultUri = insertStreamItemPhoto(streamItemId, photoValues, null);
+        long streamItemPhotoId = ContentUris.parseId(resultUri);
+
+        photoValues.put(StreamItemPhotos.PICTURE, "ABCDEFG".getBytes());
+        Uri photoUri =
+                ContentUris.withAppendedId(
+                        Uri.withAppendedPath(
+                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                        streamItemPhotoId);
+        mResolver.update(photoUri, photoValues, null, null);
+        assertStoredValues(photoUri, photoValues);
+    }
+
+    public void testUpdateStreamItemPhotoWithContentValues() {
+        long rawContactId = createRawContact();
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, null);
+        long streamItemId = ContentUris.parseId(resultUri);
+        ContentValues photoValues = buildGenericStreamItemPhotoValues(1);
+        resultUri = insertStreamItemPhoto(streamItemId, photoValues, null);
+        long streamItemPhotoId = ContentUris.parseId(resultUri);
+
+        photoValues.put(StreamItemPhotos._ID, streamItemPhotoId);
+        photoValues.put(StreamItemPhotos.PICTURE, "ABCDEFG".getBytes());
+        Uri photoUri =
+                Uri.withAppendedPath(
+                        ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                        StreamItems.StreamItemPhotos.CONTENT_DIRECTORY);
+        mResolver.update(photoUri, photoValues, null, null);
+        assertStoredValues(photoUri, photoValues);
+    }
+
+    public void testUpdateStreamItemPhotoFromOtherAccount() {
+        long rawContactId = createRawContact(mAccount);
+        ContentValues values = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, values, mAccount);
+        long streamItemId = ContentUris.parseId(resultUri);
+        ContentValues photoValues = buildGenericStreamItemPhotoValues(1);
+        resultUri = insertStreamItemPhoto(streamItemId, photoValues, mAccount);
+        long streamItemPhotoId = ContentUris.parseId(resultUri);
+
+        photoValues.put(StreamItemPhotos._ID, streamItemPhotoId);
+        photoValues.put(StreamItemPhotos.PICTURE, "ABCDEFG".getBytes());
+        Uri photoUri =
+                maybeAddAccountQueryParameters(
+                        Uri.withAppendedPath(
+                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                        mAccountTwo);
+        try {
+            mResolver.update(photoUri, photoValues, null, null);
+            fail("Should not be able to update stream item photos inserted by another account");
+        } catch (SecurityException expected) {
+            // Can't update a stream item photo inserted by another account.
+        }
+    }
+
+    // Stream item deletion test cases.
+
+    public void testDeleteStreamItemById() {
+        long rawContactId = createRawContact();
+        ContentValues firstValues = buildGenericStreamItemValues();
+        Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
+        long firstStreamItemId = ContentUris.parseId(resultUri);
+
+        ContentValues secondValues = buildGenericStreamItemValues();
+        secondValues.put(StreamItems.TEXT, "Goodbye world");
+        insertStreamItem(rawContactId, secondValues, null);
+
+        // Delete the first stream item.
+        mResolver.delete(ContentUris.withAppendedId(StreamItems.CONTENT_URI, firstStreamItemId),
+                null, null);
+
+        // Check that only the second item remains.
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.StreamItems.CONTENT_DIRECTORY), secondValues);
+    }
+
+    public void testDeleteStreamItemWithSelection() {
+        long rawContactId = createRawContact();
+        ContentValues firstValues = buildGenericStreamItemValues();
+        insertStreamItem(rawContactId, firstValues, null);
+
+        ContentValues secondValues = buildGenericStreamItemValues();
+        secondValues.put(StreamItems.TEXT, "Goodbye world");
+        insertStreamItem(rawContactId, secondValues, null);
+
+        // Delete the first stream item with a custom selection.
+        mResolver.delete(StreamItems.CONTENT_URI, StreamItems.TEXT + "=?",
+                new String[]{"Hello world"});
+
+        // Check that only the second item remains.
+        assertStoredValues(Uri.withAppendedPath(
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                RawContacts.StreamItems.CONTENT_DIRECTORY), secondValues);
+    }
+
+    public void testDeleteStreamItemFromOtherAccount() {
+        long rawContactId = createRawContact(mAccount);
+        long streamItemId = ContentUris.parseId(
+                insertStreamItem(rawContactId, buildGenericStreamItemValues(), mAccount));
+        try {
+            mResolver.delete(
+                    maybeAddAccountQueryParameters(
+                            ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                            mAccountTwo), null, null);
+            fail("Should not be able to delete stream item inserted by another account");
+        } catch (SecurityException expected) {
+            // Can't delete a stream item from another account.
+        }
+    }
+
+    // Stream item photo deletion test cases.
+
+    public void testDeleteStreamItemPhotoById() {
+        long rawContactId = createRawContact();
+        long streamItemId = ContentUris.parseId(
+                insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
+        long streamItemPhotoId = ContentUris.parseId(
+                insertStreamItemPhoto(streamItemId, buildGenericStreamItemPhotoValues(0), null));
+        mResolver.delete(
+                ContentUris.withAppendedId(
+                        Uri.withAppendedPath(
+                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                        streamItemPhotoId), null, null);
+
+        Cursor c = mResolver.query(StreamItems.CONTENT_PHOTO_URI,
+                new String[]{StreamItemPhotos._ID},
+                StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{String.valueOf(streamItemId)},
+                null);
+        try {
+            assertEquals("Expected photo to be deleted.", 0, c.getCount());
+        } finally {
+            c.close();
+        }
+    }
+
+    public void testDeleteStreamItemPhotoWithSelection() {
+        long rawContactId = createRawContact();
+        long streamItemId = ContentUris.parseId(
+                insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
+        ContentValues firstPhotoValues = buildGenericStreamItemPhotoValues(0);
+        ContentValues secondPhotoValues = buildGenericStreamItemPhotoValues(1);
+        insertStreamItemPhoto(streamItemId, firstPhotoValues, null);
+        insertStreamItemPhoto(streamItemId, secondPhotoValues, null);
+        Uri photoUri = Uri.withAppendedPath(
+                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY);
+        mResolver.delete(photoUri, StreamItemPhotos.SORT_INDEX + "=1", null);
+
+        assertStoredValues(photoUri, firstPhotoValues);
+    }
+
+    public void testDeleteStreamItemPhotoFromOtherAccount() {
+        long rawContactId = createRawContact(mAccount);
+        long streamItemId = ContentUris.parseId(
+                insertStreamItem(rawContactId, buildGenericStreamItemValues(), mAccount));
+        insertStreamItemPhoto(streamItemId, buildGenericStreamItemPhotoValues(0), mAccount);
+        try {
+            mResolver.delete(maybeAddAccountQueryParameters(
+                    Uri.withAppendedPath(
+                            ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                            StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                    mAccountTwo), null, null);
+            fail("Should not be able to delete stream item photo inserted by another account");
+        } catch (SecurityException expected) {
+            // Can't delete a stream item photo from another account.
+        }
+    }
+
+    public void testQueryStreamItemLimit() {
+        ContentValues values = new ContentValues();
+        values.put(StreamItems.MAX_ITEMS, 5);
+        values.put(StreamItems.PHOTO_MAX_BYTES, 70 * 1024);
+        assertStoredValues(StreamItems.CONTENT_LIMIT_URI, values);
+    }
+
+    private ContentValues buildGenericStreamItemValues() {
+        ContentValues values = new ContentValues();
+        values.put(StreamItems.RES_PACKAGE, "com.foo.bar");
+        values.put(StreamItems.TEXT, "Hello world");
+        values.put(StreamItems.TIMESTAMP, System.currentTimeMillis());
+        values.put(StreamItems.COMMENTS, "Reshared by 123 others");
+        return values;
+    }
+
+    private ContentValues buildGenericStreamItemPhotoValues(int sortIndex) {
+        ContentValues values = new ContentValues();
+        values.put(StreamItemPhotos.SORT_INDEX, sortIndex);
+        values.put(StreamItemPhotos.PICTURE, "DEADBEEF".getBytes());
+        return values;
+    }
+
     public void testSingleStatusUpdateRowPerContact() {
         int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
         String handle1 = "test@gmail.com";