Prototype ConversationCursor/ConversationProvider

* Handles updates/deletes from the list instantly, then forwards the request
  to the underlying provider
* Only starred/delete are implemented currently for Email types; the Gmail
  provider doesn't yet support updates to UIProvider uri's
  Note: Email types support read there's no read/unread support in the prototype
  UI
* Updated UIProvider/MockUiProvider with latest adds to Conversation class
* Underlying provider must notify ConversationCursor of changes via newly
  defined notifier URI

TODO: ConversationCursor wants unit tests

Change-Id: I91babcd5c27109acaa1f7479d584524e8a508a56
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8e0a831..6586ef6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -53,6 +53,14 @@
         </provider>
 
         <provider
+            android:authorities="com.android.mail.conversation.provider"
+            android:label="@string/conversation_content_provider"
+            android:multiprocess="false"
+            android:name=".browse.ConversationCursor$ConversationProvider" >
+            <grant-uri-permission android:pathPattern=".*" />
+        </provider>
+
+        <provider
             android:authorities="com.android.mail.accountcache"
             android:label="@string/account_cache_provider"
             android:multiprocess="false"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 29fc936..27b21d0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -22,6 +22,7 @@
     <string name="test_labelspinner" translate="false">Test Label Spinner Layout</string>
     <string name="test_endtoend" translate="false">Test End to End with provider</string>
     <string name="mock_content_provider">Mock Content Provider</string>
+    <string name="conversation_content_provider">Conversation Content Provider</string>
     <string name="account_cache_provider">Account Cache Provider</string>
     <string name="dummy_gmail_provider">Dummy Gmail Provider</string>
     <string name="test_actionbar" translate="false">Test Actionbar Layout</string>
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
new file mode 100644
index 0000000..f793b26
--- /dev/null
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -0,0 +1,472 @@
+/*******************************************************************************
+ *      Copyright (C) 2012 Google Inc.
+ *      Licensed to The Android Open Source Project.
+ *
+ *      Licensed under the Apache License, Version 2.0 (the "License");
+ *      you may not use this file except in compliance with the License.
+ *      You may obtain a copy of the License at
+ *
+ *           http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *      Unless required by applicable law or agreed to in writing, software
+ *      distributed under the License is distributed on an "AS IS" BASIS,
+ *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *      See the License for the specific language governing permissions and
+ *      limitations under the License.
+ *******************************************************************************/
+
+package com.android.mail.browse;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.CursorAdapter;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete
+ * caching for quick UI response. This is effectively a singleton class, as the cache is
+ * implemented as a static HashMap.
+ */
+public class ConversationCursor extends CursorWrapper {
+    private static final String TAG = "ConversationCursor";
+    private static final boolean DEBUG = true;  // STOPSHIP Set to false before shipping
+
+    // The authority of our conversation provider (a forwarding provider)
+    // This string must match the declaration in AndroidManifest.xml
+    private static final String sAuthority = "com.android.mail.conversation.provider";
+
+    // A mapping from Uri to updated ContentValues
+    private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>();
+    // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map
+    private static final String DELETED_COLUMN = "__deleted__";
+    // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid
+    private static final int DELETED_COLUMN_INDEX = -1;
+
+    // The cursor underlying the caching cursor
+    private final Cursor mUnderlying;
+    // Column names for this cursor
+    private final String[] mColumnNames;
+    // The index of the Uri whose data is reflected in the cached row
+    // Updates/Deletes to this Uri are cached
+    private final int mUriColumnIndex;
+    // The resolver for the cursor instantiator's context
+    private static ContentResolver mResolver;
+    // An observer on the underlying cursor (so we can detect changes from outside the UI)
+    private final CursorObserver mCursorObserver;
+    // The adapter using this cursor (which needs to refresh when data changes)
+    private static CursorAdapter mAdapter;
+
+    // The current position of the cursor
+    private int mPosition = -1;
+    // The number of cached deletions from this cursor (used to quickly generate an accurate count)
+    private static int sDeletedCount = 0;
+
+    public ConversationCursor(Cursor cursor, Context context, String messageListColumn) {
+        super(cursor);
+        mUnderlying = cursor;
+        mCursorObserver = new CursorObserver();
+        // New cursor -> clear the cache
+        resetCache();
+        mColumnNames = cursor.getColumnNames();
+        mUriColumnIndex = getColumnIndex(messageListColumn);
+        if (mUriColumnIndex < 0) {
+            throw new IllegalArgumentException("Cursor must include a message list column");
+        }
+        mResolver = context.getContentResolver();
+        // We'll observe the underlying cursor and act when it changes
+        //cursor.registerContentObserver(mCursorObserver);
+    }
+
+    /**
+     * Reset the cache; this involves clearing out our cache map and resetting our various counts
+     * The cache should be reset whenever we get fresh data from the underlying cursor
+     */
+    private void resetCache() {
+        sCacheMap.clear();
+        sDeletedCount = 0;
+        mPosition = -1;
+        mUnderlying.registerContentObserver(mCursorObserver);
+    }
+
+    /**
+     * Set the adapter for this cursor; we'll notify it when our data changes
+     */
+    public void setAdapter(CursorAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Generate a forwarding Uri to ConversationProvider from an original Uri.  We do this by
+     * changing the authority to ours, but otherwise leaving the Uri intact.
+     * NOTE: This won't handle query parameters, so the functionality will need to be added if
+     * parameters are used in the future
+     * @param uri the uri
+     * @return a forwarding uri to ConversationProvider
+     */
+    private static String uriToCachingUriString (Uri uri) {
+        String provider = uri.getAuthority();
+        return uri.getScheme() + "://" + sAuthority + "/" + provider + uri.getPath();
+    }
+
+    /**
+     * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
+     * NOTE: See note above for uriToCachingUri
+     * @param uri the forwarding Uri
+     * @return the original Uri
+     */
+    private static Uri uriFromCachingUri(Uri uri) {
+        List<String> path = uri.getPathSegments();
+        Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
+        for (int i = 1; i < path.size(); i++) {
+            builder.appendPath(path.get(i));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Cache a column name/value pair for a given Uri
+     * @param uriString the Uri for which the column name/value pair applies
+     * @param columnName the column name
+     * @param value the value to be cached
+     */
+    private static void cacheValue(String uriString, String columnName, Object value) {
+        // Get the map for our uri
+        ContentValues map = sCacheMap.get(uriString);
+        // Create one if necessary
+        if (map == null) {
+            map = new ContentValues();
+            sCacheMap.put(uriString, map);
+        }
+        // If we're caching a deletion, add to our count
+        if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) {
+            sDeletedCount++;
+            if (DEBUG) {
+                Log.d(TAG, "Deleted " + uriString);
+            }
+        }
+        // ContentValues has no generic "put", so we must test.  For now, the only classes of
+        // values implemented are Boolean/Integer/String, though others are trivially added
+        if (value instanceof Boolean) {
+            map.put(columnName, ((Boolean)value).booleanValue() ? 1 : 0);
+        } else if (value instanceof Integer) {
+            map.put(columnName, (Integer)value);
+        } else if (value instanceof String) {
+            map.put(columnName, (String)value);
+        } else {
+            String cname = value.getClass().getName();
+            throw new IllegalArgumentException("Value class not compatible with cache: " + cname);
+        }
+
+        // Since we've changed the data, alert the adapter to redraw
+        mAdapter.notifyDataSetChanged();
+        if (DEBUG && (columnName != DELETED_COLUMN)) {
+            Log.d(TAG, "Caching value for " + uriString + ": " + columnName);
+        }
+    }
+
+    /**
+     * Get the cached value for the provided column; we special case -1 as the "deleted" column
+     * @param columnIndex the index of the column whose cached value we want to retrieve
+     * @return the cached value for this column, or null if there is none
+     */
+    private Object getCachedValue(int columnIndex) {
+        String uri = super.getString(mUriColumnIndex);
+        ContentValues uriMap = sCacheMap.get(uri);
+        if (uriMap != null) {
+            String columnName;
+            if (columnIndex == DELETED_COLUMN_INDEX) {
+                columnName = DELETED_COLUMN;
+            } else {
+                columnName = mColumnNames[columnIndex];
+            }
+            return uriMap.get(columnName);
+        }
+        return null;
+    }
+
+    /**
+     * When the underlying cursor changes, we want to force a requery to get the new provider data;
+     * the cache must also be reset here since it's no longer fresh
+     */
+    private void underlyingChanged() {
+        super.requery();
+        resetCache();
+    }
+
+    // We don't want to do anything when we get a requery, as our data is updated immediately from
+    // the UI and we detect changes on the underlying provider above
+    public boolean requery() {
+        return true;
+    }
+
+    public void close() {
+        // Unregister our observer on the underlying cursor and close as usual
+        mUnderlying.unregisterContentObserver(mCursorObserver);
+        super.close();
+    }
+
+    /**
+     * Move to the next not-deleted item in the conversation
+     */
+    public boolean moveToNext() {
+        while (true) {
+            boolean ret = super.moveToNext();
+            if (!ret) return false;
+            if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
+            mPosition++;
+            return true;
+        }
+    }
+
+    /**
+     * Move to the previous not-deleted item in the conversation
+     */
+    public boolean moveToPrevious() {
+        while (true) {
+            boolean ret = super.moveToPrevious();
+            if (!ret) return false;
+            if (getCachedValue(-1) instanceof Integer) continue;
+            mPosition--;
+            return true;
+        }
+    }
+
+    public int getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * The actual cursor's count must be decremented by the number we've deleted from the UI
+     */
+    public int getCount() {
+        return super.getCount() - sDeletedCount;
+    }
+
+    public boolean moveToFirst() {
+        super.moveToPosition(-1);
+        mPosition = -1;
+        return moveToNext();
+    }
+
+    public boolean moveToPosition(int pos) {
+        if (pos == mPosition) return true;
+        if (pos > mPosition) {
+            while (pos > mPosition) {
+                if (!moveToNext()) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (pos == 0) {
+            return moveToFirst();
+        } else {
+            while (pos < mPosition) {
+                if (!moveToPrevious()) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    public boolean moveToLast() {
+        throw new UnsupportedOperationException("moveToLast unsupported!");
+    }
+
+    public boolean move(int offset) {
+        throw new UnsupportedOperationException("move unsupported!");
+    }
+
+    /**
+     * We need to override all of the getters to make sure they look at cached values before using
+     * the values in the underlying cursor
+     */
+    @Override
+    public double getDouble(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (Double)obj;
+        return super.getDouble(columnIndex);
+    }
+
+    @Override
+    public float getFloat(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (Float)obj;
+        return super.getFloat(columnIndex);
+    }
+
+    @Override
+    public int getInt(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (Integer)obj;
+        return super.getInt(columnIndex);
+    }
+
+    @Override
+    public long getLong(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (Long)obj;
+        return super.getLong(columnIndex);
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (Short)obj;
+        return super.getShort(columnIndex);
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        // If we're asking for the Uri for the conversation list, we return a forwarding URI
+        // so that we can intercept update/delete and handle it ourselves
+        if (columnIndex == mUriColumnIndex) {
+            Uri uri = Uri.parse(super.getString(columnIndex));
+            return uriToCachingUriString(uri);
+        }
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (String)obj;
+        return super.getString(columnIndex);
+    }
+
+    @Override
+    public byte[] getBlob(int columnIndex) {
+        Object obj = getCachedValue(columnIndex);
+        if (obj != null) return (byte[])obj;
+        return super.getBlob(columnIndex);
+    }
+
+    /**
+     * Observer of changes to underlying data
+     */
+    private class CursorObserver extends ContentObserver {
+        public CursorObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            // If we're here, then something outside of the UI has changed the data, and we
+            // must requery to get that data from the underlying provider
+            if (DEBUG) {
+                Log.d(TAG, "Underlying conversation cursor changed; requerying");
+            }
+            // It's not at all obvious to me why we must unregister/re-register after the requery
+            // However, if we don't we'll only get one notification and no more...
+            mUnderlying.unregisterContentObserver(mCursorObserver);
+            ConversationCursor.this.underlyingChanged();
+        }
+    }
+
+    /**
+     * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
+     * and inserts directly, and caches updates/deletes before passing them through.  The caching
+     * will cause a redraw of the list with updated values.
+     */
+    public static class ConversationProvider extends ContentProvider {
+        @Override
+        public boolean onCreate() {
+            return false;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return mResolver.query(
+                    uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
+        }
+
+        @Override
+        public String getType(Uri uri) {
+            return null;
+        }
+
+        /**
+         * Quick and dirty class that executes underlying provider CRUD operations on a background
+         * thread.
+         */
+        static class ProviderExecute implements Runnable {
+            static final int DELETE = 0;
+            static final int INSERT = 1;
+            static final int UPDATE = 2;
+
+            final int mCode;
+            final Uri mUri;
+            final ContentValues mValues; //HEHEH
+
+            ProviderExecute(int code, Uri uri, ContentValues values) {
+                mCode = code;
+                mUri = uriFromCachingUri(uri);
+                mValues = values;
+            }
+
+            ProviderExecute(int code, Uri uri) {
+                this(code, uri, null);
+            }
+
+            static void opDelete(Uri uri) {
+                new Thread(new ProviderExecute(DELETE, uri)).start();
+            }
+
+            static void opInsert(Uri uri, ContentValues values) {
+                new Thread(new ProviderExecute(INSERT, uri, values)).start();
+            }
+
+            static void opUpdate(Uri uri, ContentValues values) {
+                new Thread(new ProviderExecute(UPDATE, uri, values)).start();
+            }
+
+            @Override
+            public void run() {
+                switch(mCode) {
+                    case DELETE:
+                        mResolver.delete(mUri, null, null);
+                        break;
+                    case INSERT:
+                        mResolver.insert(mUri, mValues);
+                        break;
+                    case UPDATE:
+                        mResolver.update(mUri,  mValues, null, null);
+                        break;
+                }
+            }
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            ProviderExecute.opInsert(uri, values);
+            return null;
+        }
+
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            Uri underlyingUri = uriFromCachingUri(uri);
+            String uriString = underlyingUri.toString();
+            cacheValue(uriString, DELETED_COLUMN, true);
+            ProviderExecute.opDelete(uri);
+            return 0;
+        }
+
+        @Override
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            Uri underlyingUri = uriFromCachingUri(uri);
+            // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
+            String uriString =  Uri.decode(underlyingUri.toString());
+            for (String columnName: values.keySet()) {
+                cacheValue(uriString, columnName, values.get(columnName));
+            }
+            ProviderExecute.opUpdate(uri, values);
+            return 0;
+        }
+    }
+}
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 27fa725..f18a433 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -52,6 +52,7 @@
 import com.android.mail.perf.Timer;
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
+import com.android.mail.providers.UIProvider.ConversationColumns;
 import com.android.mail.utils.Utils;
 
 public class ConversationItemView extends View {
@@ -784,6 +785,8 @@
         postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
                 + mHeader.starBitmap.getWidth(),
                 mCoordinates.starY + mHeader.starBitmap.getHeight());
+        // Generalize this...
+        mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
     }
 
     private boolean touchCheckmark(float x, float y) {
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index afab608..8829297 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -32,6 +32,8 @@
 
 import com.android.mail.R;
 import com.android.mail.providers.Conversation;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.providers.UIProvider.ConversationFlags;
 
 import java.util.ArrayList;
 
@@ -118,7 +120,9 @@
         if (cursor != null) {
             header.faded = false;
             header.checkboxVisible = true;
-            header.conversation = Conversation.from(cursor);
+            Conversation conv = Conversation.from(cursor);
+            header.conversation = conv;
+            header.starred = conv.starred;
         }
         return header;
     }
diff --git a/src/com/android/mail/browse/ConversationListActivity.java b/src/com/android/mail/browse/ConversationListActivity.java
index a981b19..ec8dd89 100644
--- a/src/com/android/mail/browse/ConversationListActivity.java
+++ b/src/com/android/mail/browse/ConversationListActivity.java
@@ -30,6 +30,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.CursorAdapter;
 import android.widget.ListView;
 import android.widget.SimpleCursorAdapter;
@@ -48,7 +49,7 @@
 import com.android.mail.providers.UIProvider;
 
 public class ConversationListActivity extends Activity implements OnItemSelectedListener,
-        OnItemClickListener {
+        OnItemClickListener, OnItemLongClickListener {
 
     private ListView mListView;
     private ConversationItemAdapter mListAdapter;
@@ -63,6 +64,7 @@
         setContentView(R.layout.conversation_list_activity);
         mListView = (ListView) findViewById(R.id.browse_list);
         mListView.setOnItemClickListener(this);
+        mListView.setOnItemLongClickListener(this);
         mAccountsSpinner = (Spinner) findViewById(R.id.accounts_spinner);
         mResolver = getContentResolver();
         Cursor cursor = mResolver.query(AccountCacheProvider.getAccountsUri(),
@@ -125,11 +127,14 @@
 
     class ConversationItemAdapter extends SimpleCursorAdapter {
 
-        public ConversationItemAdapter(Context context, int textViewResourceId, Cursor cursor) {
+        public ConversationItemAdapter(Context context, int textViewResourceId,
+                ConversationCursor cursor) {
             // Set requery/observer flags temporarily; we will be using loaders eventually so
             // this is just a temporary hack to demonstrate push, etc.
             super(context, textViewResourceId, cursor, UIProvider.CONVERSATION_PROJECTION, null,
                     CursorAdapter.FLAG_AUTO_REQUERY | CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
+            // UpdateCachingCursor needs to know about the adapter
+            cursor.setAdapter(this);
         }
 
         @Override
@@ -147,6 +152,7 @@
 
     @Override
     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        // Get an account and a folder list
         Uri foldersUri = null;
         Cursor cursor = mAccountsAdapter.getCursor();
         if (cursor != null && cursor.moveToPosition(position)) {
@@ -165,12 +171,17 @@
                 cursor.close();
             }
         }
-        if (conversationListUri != null) {
-            cursor = mResolver.query(conversationListUri, UIProvider.CONVERSATION_PROJECTION, null,
-                    null, null);
+        // We need to have a conversation list here...
+        if (conversationListUri == null) {
+            throw new IllegalStateException("No conversation list for this account");
         }
+        // Create the cursor for the list using the update cache
+        ConversationCursor conversationListCursor =
+                new ConversationCursor(
+                    mResolver.query(conversationListUri, UIProvider.CONVERSATION_PROJECTION, null,
+                            null, null), this, UIProvider.ConversationColumns.MESSAGE_LIST_URI);
         mListAdapter = new ConversationItemAdapter(this, R.layout.conversation_item_view_normal,
-                cursor);
+                conversationListCursor);
         mListView.setAdapter(mListAdapter);
     }
 
@@ -183,4 +194,12 @@
         Conversation conv = ((ConversationItemView) view).getConversation();
         ConversationViewActivity.viewConversation(this, conv, mSelectedAccount);
     }
+
+    // Temporary to test deletion (we'll delete the convo on long click)
+    @Override
+    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+        Conversation conv = ((ConversationItemView) view).getConversation();
+        conv.delete(this);
+        return true;
+    }
 }
diff --git a/src/com/android/mail/providers/Conversation.java b/src/com/android/mail/providers/Conversation.java
index 91ad0b9..2854981 100644
--- a/src/com/android/mail/providers/Conversation.java
+++ b/src/com/android/mail/providers/Conversation.java
@@ -16,6 +16,8 @@
 
 package com.android.mail.providers;
 
+import android.content.ContentValues;
+import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Parcel;
@@ -34,6 +36,8 @@
     public int numDrafts;
     public int sendingState;
     public int priority;
+    public boolean read;
+    public boolean starred;
 
     @Override
     public int describeContents() {
@@ -53,6 +57,8 @@
         dest.writeInt(numDrafts);
         dest.writeInt(sendingState);
         dest.writeInt(priority);
+        dest.writeByte(read ? (byte) 1 : 0);
+        dest.writeByte(starred ? (byte) 1 : 0);
     }
 
     private Conversation(Parcel in) {
@@ -67,6 +73,8 @@
         numDrafts = in.readInt();
         sendingState = in.readInt();
         priority = in.readInt();
+        read = (in.readByte() != 0);
+        starred = (in.readByte() != 0);
     }
 
     @Override
@@ -98,8 +106,7 @@
             dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
             subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
             snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
-            hasAttachments = cursor
-                    .getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) == 1 ? true : false;
+            hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) == 1;
             messageListUri = Uri.parse(cursor
                     .getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN));
             senders = cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN);
@@ -107,7 +114,22 @@
             numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
             sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
             priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
+            read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) == 1;
+            starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) == 1;
         }
     }
 
+    // Below are methods that update Conversation data (update/delete)
+
+    public void updateBoolean(Context context, String columnName, boolean value) {
+        // For now, synchronous
+        ContentValues cv = new ContentValues();
+        cv.put(columnName, value);
+        context.getContentResolver().update(messageListUri, cv, null, null);
+    }
+
+    public void delete(Context context) {
+        // For now, synchronous
+        context.getContentResolver().delete(messageListUri, null, null);
+    }
 }
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index 8fcfe2b..e1121d3 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -161,7 +161,7 @@
         FolderColumns.CONVERSATION_LIST_URI,
         FolderColumns.CHILD_FOLDERS_LIST_URI,
         FolderColumns.UNREAD_COUNT,
-        FolderColumns.TOTAL_COUNT
+        FolderColumns.TOTAL_COUNT,
     };
 
     public static final int FOLDER_ID_COLUMN = 0;
@@ -243,7 +243,9 @@
         ConversationColumns.NUM_MESSAGES,
         ConversationColumns.NUM_DRAFTS,
         ConversationColumns.SENDING_STATE,
-        ConversationColumns.PRIORITY
+        ConversationColumns.PRIORITY,
+        ConversationColumns.READ,
+        ConversationColumns.STARRED
     };
 
     // These column indexes only work when the caller uses the
@@ -260,6 +262,8 @@
     public static final int CONVERSATION_NUM_DRAFTS_COLUMN = 9;
     public static final int CONVERSATION_SENDING_STATE_COLUMN = 10;
     public static final int CONVERSATION_PRIORITY_COLUMN = 11;
+    public static final int CONVERSATION_READ_COLUMN = 12;
+    public static final int CONVERSATION_STARRED_COLUMN = 13;
 
     public static final class ConversationSendingState {
         public static final int OTHER = 0;
@@ -273,6 +277,13 @@
         public static final int HIGH = 1;
     };
 
+    public static final class ConversationFlags {
+        public static final int READ = 1<<0;
+        public static final int STARRED = 1<<1;
+        public static final int REPLIED = 1<<2;
+        public static final int FORWARDED = 1<<3;
+    };
+
     public static final class ConversationColumns {
         public static final String URI = "conversationUri";
         /**
@@ -330,6 +341,16 @@
          */
         public static String PRIORITY = "priority";
 
+        /**
+         * This boolean column indicates whether the conversation has been read
+         */
+        public static String READ = "read";
+
+        /**
+         * This boolean column indicates whether the conversation has been read
+         */
+        public static String STARRED = "starred";
+
         public ConversationColumns() {
         }
     }
diff --git a/src/com/android/mail/providers/protos/mock/MockUiProvider.java b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
index 8ef5eec..56ae8ba 100644
--- a/src/com/android/mail/providers/protos/mock/MockUiProvider.java
+++ b/src/com/android/mail/providers/protos/mock/MockUiProvider.java
@@ -178,6 +178,8 @@
         conversationMap.put(ConversationColumns.NUM_MESSAGES, 1);
         conversationMap.put(ConversationColumns.NUM_DRAFTS, 1);
         conversationMap.put(ConversationColumns.SENDING_STATE, 1);
+        conversationMap.put(ConversationColumns.READ, 0);
+        conversationMap.put(ConversationColumns.STARRED, 0);
         return conversationMap;
     }