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 
17 package com.android.email.provider;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.database.CursorWrapper;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.text.TextUtils;
27 import android.text.format.DateUtils;
28 import android.text.util.Rfc822Token;
29 import android.text.util.Rfc822Tokenizer;
30 
31 import com.android.emailcommon.Logging;
32 import com.android.emailcommon.mail.Address;
33 import com.android.emailcommon.provider.EmailContent;
34 import com.android.emailcommon.provider.Mailbox;
35 import com.android.mail.browse.ConversationCursorOperationListener;
36 import com.android.mail.providers.ConversationInfo;
37 import com.android.mail.providers.Folder;
38 import com.android.mail.providers.FolderList;
39 import com.android.mail.providers.ParticipantInfo;
40 import com.android.mail.providers.UIProvider;
41 import com.android.mail.providers.UIProvider.ConversationColumns;
42 import com.android.mail.utils.LogUtils;
43 import 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  */
50 public 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 
EmailConversationCursor(final Context context, final Cursor cursor, final Folder folder, final long mailboxId)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
getExtras()115     public Bundle getExtras() {
116         return mExtras;
117     }
118 
119     @Override
respond(Bundle params)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 
generateConversationInfo()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
markContentsSeen()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
emptyFolder()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