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";