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