1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.database.CursorWrapper;
24import android.net.Uri;
25import android.os.Bundle;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.text.util.Rfc822Token;
29import android.text.util.Rfc822Tokenizer;
30
31import com.android.emailcommon.Logging;
32import com.android.emailcommon.mail.Address;
33import com.android.emailcommon.provider.EmailContent;
34import com.android.emailcommon.provider.Mailbox;
35import com.android.mail.browse.ConversationCursorOperationListener;
36import com.android.mail.providers.ConversationInfo;
37import com.android.mail.providers.Folder;
38import com.android.mail.providers.FolderList;
39import com.android.mail.providers.ParticipantInfo;
40import com.android.mail.providers.UIProvider;
41import com.android.mail.providers.UIProvider.ConversationColumns;
42import com.android.mail.utils.LogUtils;
43import com.google.common.collect.Lists;
44
45/**
46 * Wrapper that handles the visibility feature (i.e. the conversation list is visible, so
47 * any pending notifications for the corresponding mailbox should be canceled). We also handle
48 * getExtras() to provide a snapshot of the mailbox's status
49 */
50public class EmailConversationCursor extends CursorWrapper implements
51        ConversationCursorOperationListener {
52    private final long mMailboxId;
53    private final int mMailboxTypeId;
54    private final Context mContext;
55    private final FolderList mFolderList;
56    private final Bundle mExtras = new Bundle();
57
58    /**
59     * When showing a folder, if it's been at least this long since the last sync,
60     * force a folder refresh.
61     */
62    private static final long AUTO_REFRESH_INTERVAL_MS = 5 * DateUtils.MINUTE_IN_MILLIS;
63
64    public EmailConversationCursor(final Context context, final Cursor cursor,
65            final Folder folder, final long mailboxId) {
66        super(cursor);
67        mMailboxId = mailboxId;
68        mContext = context;
69        mFolderList = FolderList.copyOf(Lists.newArrayList(folder));
70        Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId);
71
72        if (mailbox != null) {
73            mMailboxTypeId = mailbox.mType;
74
75            mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT, mailbox.mTotalCount);
76            if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_BACKGROUND
77                    || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_USER
78                    || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_LIVE
79                    || mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_INITIAL_SYNC_NEEDED) {
80                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
81                        UIProvider.CursorStatus.LOADING);
82            } else if (mailbox.mUiSyncStatus == EmailContent.SYNC_STATUS_NONE) {
83                if (mailbox.mSyncInterval == 0
84                        && (Mailbox.isSyncableType(mailbox.mType)
85                        || mailbox.mType == Mailbox.TYPE_SEARCH)
86                        && !TextUtils.isEmpty(mailbox.mServerId) &&
87                        // TODO: There's potentially a race condition here.
88                        // Consider merging this check with the auto-sync code in respond.
89                        System.currentTimeMillis() - mailbox.mSyncTime
90                                > AUTO_REFRESH_INTERVAL_MS) {
91                    // This will be syncing momentarily
92                    mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
93                            UIProvider.CursorStatus.LOADING);
94                } else {
95                    mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
96                            UIProvider.CursorStatus.COMPLETE);
97                }
98            } else {
99                LogUtils.d(Logging.LOG_TAG,
100                        "Unknown mailbox sync status" + mailbox.mUiSyncStatus);
101                mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
102                        UIProvider.CursorStatus.COMPLETE);
103            }
104        } else {
105            mMailboxTypeId = -1;
106            // TODO for virtual mailboxes, we may want to do something besides just fake it
107            mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_TOTAL_COUNT,
108                    cursor != null ? cursor.getCount() : 0);
109            mExtras.putInt(UIProvider.CursorExtraKeys.EXTRA_STATUS,
110                    UIProvider.CursorStatus.COMPLETE);
111        }
112    }
113
114    @Override
115    public Bundle getExtras() {
116        return mExtras;
117    }
118
119    @Override
120    public Bundle respond(Bundle params) {
121        final String setVisibilityKey =
122                UIProvider.ConversationCursorCommand.COMMAND_KEY_SET_VISIBILITY;
123        if (params.containsKey(setVisibilityKey)) {
124            final boolean visible = params.getBoolean(setVisibilityKey);
125            if (visible) {
126                // Mark all messages as seen
127                markContentsSeen();
128                if (params.containsKey(
129                        UIProvider.ConversationCursorCommand.COMMAND_KEY_ENTERED_FOLDER)) {
130                    Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
131                    if (mailbox != null) {
132                        // For non-push mailboxes, if it's stale (i.e. last sync was a while
133                        // ago), force a sync.
134                        // TODO: Fix the check for whether we're non-push? Right now it checks
135                        // whether we are participating in account sync rules.
136                        if (mailbox.mSyncInterval == 0) {
137                            final long timeSinceLastSync =
138                                    System.currentTimeMillis() - mailbox.mSyncTime;
139                            if (timeSinceLastSync > AUTO_REFRESH_INTERVAL_MS) {
140                                final ContentResolver resolver = mContext.getContentResolver();
141                                final Uri refreshUri = Uri.parse(EmailContent.CONTENT_URI +
142                                        "/" + EmailProvider.QUERY_UIREFRESH + "/" + mailbox.mId);
143                                resolver.query(refreshUri, null, null, null, null);
144                            }
145                        }
146                    }
147                }
148            }
149        }
150        // Return success
151        final Bundle response = new Bundle(2);
152
153        response.putString(setVisibilityKey,
154                UIProvider.ConversationCursorCommand.COMMAND_RESPONSE_OK);
155
156        final String rawFoldersKey =
157                UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS;
158        if (params.containsKey(rawFoldersKey)) {
159            response.putParcelable(rawFoldersKey, mFolderList);
160        }
161
162        final String convInfoKey =
163                UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO;
164        if (params.containsKey(convInfoKey)) {
165            response.putParcelable(convInfoKey, generateConversationInfo());
166        }
167
168        return response;
169    }
170
171    private ConversationInfo generateConversationInfo() {
172        final int numMessages = getInt(getColumnIndex(ConversationColumns.NUM_MESSAGES));
173        final ConversationInfo conversationInfo = new ConversationInfo(numMessages);
174
175        conversationInfo.firstSnippet = getString(getColumnIndex(ConversationColumns.SNIPPET));
176        conversationInfo.lastSnippet = conversationInfo.firstSnippet;
177        conversationInfo.firstUnreadSnippet = conversationInfo.firstSnippet;
178
179        final boolean isRead = getInt(getColumnIndex(ConversationColumns.READ)) != 0;
180        final String senderString = getString(getColumnIndex(EmailContent.MessageColumns.DISPLAY_NAME));
181
182        final String fromString = getString(getColumnIndex(EmailContent.MessageColumns.FROM_LIST));
183        final String senderEmail;
184
185        if (fromString != null) {
186            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(fromString);
187            if (tokens.length > 0) {
188                senderEmail = tokens[0].getAddress();
189            } else {
190                LogUtils.d(LogUtils.TAG, "Couldn't parse sender email address");
191                senderEmail = fromString;
192            }
193        } else {
194            senderEmail = null;
195        }
196
197        // we *intentionally* report no participants for Draft emails so that the UI always
198        // displays the single word "Draft" as per b/13304929
199        if (mMailboxTypeId == Mailbox.TYPE_DRAFTS) {
200            // the UI displays "Draft" in the conversation list based on this count
201            conversationInfo.draftCount = 1;
202        } else if (mMailboxTypeId == Mailbox.TYPE_SENT ||
203                mMailboxTypeId == Mailbox.TYPE_OUTBOX) {
204            // for conversations in outgoing mail mailboxes return a list of recipients
205            final String recipientsString = getString(getColumnIndex(
206                    EmailContent.MessageColumns.TO_LIST));
207            final Address[] recipientAddresses = Address.parse(recipientsString);
208            for (Address recipientAddress : recipientAddresses) {
209                final String name = recipientAddress.getSimplifiedName();
210                final String email = recipientAddress.getAddress();
211
212                // all recipients are said to have read all messages in the conversation
213                conversationInfo.addParticipant(new ParticipantInfo(name, email, 0, isRead));
214            }
215        } else {
216            // for conversations in incoming mail mailboxes return the sender
217            conversationInfo.addParticipant(new ParticipantInfo(senderString, senderEmail, 0,
218                    isRead));
219        }
220
221        return conversationInfo;
222    }
223
224    @Override
225    public void markContentsSeen() {
226        final ContentResolver resolver = mContext.getContentResolver();
227        final ContentValues contentValues = new ContentValues(1);
228        contentValues.put(EmailContent.MessageColumns.FLAG_SEEN, true);
229        final Uri uri = EmailContent.Message.CONTENT_URI;
230        final String where = EmailContent.MessageColumns.MAILBOX_KEY + " = ? AND " +
231                EmailContent.MessageColumns.FLAG_SEEN + " != ?";
232        final String[] selectionArgs = {String.valueOf(mMailboxId), "1"};
233        resolver.update(uri, contentValues, where, selectionArgs);
234    }
235
236    @Override
237    public void emptyFolder() {
238        final ContentResolver resolver = mContext.getContentResolver();
239        final Uri purgeUri = EmailProvider.uiUri("uipurgefolder", mMailboxId);
240        resolver.delete(purgeUri, null, null);
241    }
242}
243