/** M: Wipe the records from MessageStateChange table which belong to mMailbox */ private void wipeMessageStateChanges() { Cursor c = mContentResolver.query( Message.CONTENT_URI, new String[] {MessageColumns.MESSAGE_ID}, MessageColumns.MAILBOX_KEY + "=?", new String[] {String.valueOf(mMailbox.mId)}, null); if (c == null) { LogUtils.i(Eas.BSK_TAG, "Get message cursor failed"); return; } try { ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); while (c.moveToNext()) { ops.add( ContentProviderOperation.newDelete(MessageStateChange.CONTENT_URI) .withSelection( MessageChangeLogTable.MESSAGE_KEY + "=?", new String[] {c.getString(0)}) .build()); applyBatchIfNeeded(ops, MAX_OPS_PER_BATCH, false); } applyBatchIfNeeded(ops, MAX_OPS_PER_BATCH, true); } catch (RemoteException e) { LogUtils.i(Eas.BSK_TAG, "RemoteException when wipeMessageStateChanges"); } catch (OperationApplicationException e) { LogUtils.i(Eas.BSK_TAG, "OperationApplicationException when wipeMessageStateChanges"); } finally { c.close(); } }
@Deprecated @Override public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) throws RemoteException { final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); if (mailbox == null) return; final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); if (account == null) return; final EmailServiceInfo info = EmailServiceUtils.getServiceInfoForAccount(mContext, account.mId); final android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, info.accountType); final Bundle extras = Mailbox.createSyncBundle(mailboxId); if (userRequest) { extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } if (deltaMessageCount != 0) { extras.putInt(Mailbox.SYNC_EXTRA_DELTA_MESSAGE_COUNT, deltaMessageCount); } ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras); LogUtils.i( Logging.LOG_TAG, "requestSync EmailServiceStub startSync %s, %s", account.toString(), extras.toString()); }
@Override public void onLoadFinished( Loader<ObjectCursor<ConversationMessage>> loader, ObjectCursor<ConversationMessage> data) { // ignore truly duplicate results // this can happen when restoring after rotation if (mCursor == data) { return; } else { final MessageCursor messageCursor = (MessageCursor) data; // bind the cursor to this fragment so it can access to the current list controller messageCursor.setController(AbstractConversationViewFragment.this); if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump()); } // We have no messages: exit conversation view. if (messageCursor.getCount() == 0 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus()) || mIsDetached)) { if (mUserVisible) { onError(); } else { // we expect that the pager adapter will remove this // conversation fragment on its own due to a separate // conversation cursor update (we might get here if the // message list update fires first. nothing to do // because we expect to be torn down soon.) LogUtils.i( LOG_TAG, "CVF: offscreen conv has no messages, ignoring update" + " in anticipation of conv cursor update. c=%s", mConversation.uri); } // existing mCursor will imminently be closed, must stop referencing it // since we expect to be kicked out soon, it doesn't matter what mCursor // becomes mCursor = null; return; } // ignore cursors that are still loading results if (!messageCursor.isLoaded()) { // existing mCursor will imminently be closed, must stop referencing it // in this case, the new cursor is also no good, and since don't expect to get // here except in initial load situations, it's safest to just ensure the // reference is null mCursor = null; return; } final MessageCursor oldCursor = mCursor; mCursor = messageCursor; onMessageCursorLoadFinished(loader, mCursor, oldCursor); } }
private void onError() { // need to exit this view- conversation may have been // deleted, or for whatever reason is now invalid (e.g. // discard single draft) // // N.B. this may involve a fragment transaction, which // FragmentManager will refuse to execute directly // within onLoadFinished. Make sure the controller knows. LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode"); // TODO(mindyp): handle ERROR status by showing an error // message to the user that there are no messages in // this conversation popOut(); }
@Override protected void wipe() { LogUtils.i(Eas.BSK_TAG, "Wipe the change log of mailbox " + mMailbox); /** * M: With the "Bad Sync Key" recovery mechanism, we no longer wipe all the local mails * considering the network data usage for re-synchronizing. We just ignore all the local changes * before the recovery @{ */ mContentResolver.delete( MessageMove.CONTENT_URI, MessageMove.SRC_FOLDER_KEY + "=" + mMailbox.mId, null); wipeMessageStateChanges(); /** @} */ // Mailbox.resyncMailbox(mContentResolver, new // android.accounts.Account(mAccount.mEmailAddress, // Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mMailbox.mId); }
private static void syncAccount(final Context context, final Account account) { final EmailServiceUtils.EmailServiceInfo info = EmailServiceUtils.getServiceInfo(context, account.getProtocol(context)); final android.accounts.Account amAccount = new android.accounts.Account(account.mEmailAddress, info.accountType); final Bundle extras = new Bundle(3); extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true); extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras); LogUtils.i( TAG, "requestSync SecurityPolicy syncAccount %s, %s", account.toString(), extras.toString()); }
public void commitImpl(int maxOpsPerBatch) throws RemoteException, OperationApplicationException { // Use a batch operation to handle the changes ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); // Maximum size of message text per fetch int numFetched = fetchedEmails.size(); LogUtils.d( TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d " + "numDeleted=%d numChanged=%d", maxOpsPerBatch, numFetched, newEmails.size(), deletedEmails.size(), changedEmails.size()); for (EmailContent.Message msg : fetchedEmails) { // Find the original message's id (by serverId and mailbox) Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION); String id = null; try { if (c.moveToFirst()) { id = c.getString(EmailContent.ID_PROJECTION_COLUMN); while (c.moveToNext()) { // This shouldn't happen, but clean up if it does Long dupId = Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN)); userLog("Delete duplicate with id: " + dupId); deletedEmails.add(dupId); } } } finally { c.close(); } // If we find one, we do two things atomically: 1) set the body text for the // message, and 2) mark the message loaded (i.e. completely loaded) if (id != null) { LogUtils.i(TAG, "Fetched body successfully for %s", id); final String[] bindArgument = new String[] {id}; ops.add( ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI) .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument) .withValue(EmailContent.BodyColumns.TEXT_CONTENT, msg.mText) .build()); ops.add( ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI) .withSelection(MessageColumns._ID + "=?", bindArgument) .withValue(MessageColumns.FLAG_LOADED, EmailContent.Message.FLAG_LOADED_COMPLETE) .build()); } applyBatchIfNeeded(ops, maxOpsPerBatch, false); } /** M: "Bad Sync Key" recovery process @{ */ if (mMailbox.mId == Exchange.sBadSyncKeyMailboxId && newEmails.size() > 0) { LogUtils.i(Eas.BSK_TAG, "newEmails count:" + newEmails.size()); // Delete the local mails older than the oldest mail timestamp in the 1st window ExchangePreferences pref = ExchangePreferences.getPreferences(mContext); boolean isStaleMailsRemoved = pref.getRemovedStaleMails(); if (!isStaleMailsRemoved) { // Get the oldest mail's time stamp long oldestTimestamp = 0; oldestTimestamp = newEmails.get(0).mTimeStamp; for (Message msg : newEmails) { if (msg.mTimeStamp < oldestTimestamp) { oldestTimestamp = msg.mTimeStamp; } } LogUtils.i(Eas.BSK_TAG, "Oldest timestamp: " + oldestTimestamp); // Delete all the local mails older than the time stamp int rowDeleted = mContentResolver.delete( Message.CONTENT_URI, WHERE_MESSAGE_TIMESTAMP_LESS_THAN, new String[] {String.valueOf(oldestTimestamp), String.valueOf(mMailbox.mId)}); LogUtils.i(Eas.BSK_TAG, rowDeleted + " local stale mails were deleted"); // Set all mails of this mailbox as "dirty" at first. Then matching local mail // with the new mail by their timestamp. If found, clear the "dirty" flag. In // this way, we can ultimately mark the stale local mails and remove them ContentValues cv = new ContentValues(); cv.put(MessageColumns.DIRTY, "1"); mContentResolver.update( Message.CONTENT_URI, cv, WHERE_MAILBOX_KEY, new String[] {String.valueOf(mMailbox.mId)}); pref.setRemovedStaleMails(true); } // Remove the stale flag of the local mails which can be found in the new-synchronized mails // (by compare their time-stamp). Note that server id can not regarded as the identity of // a mail because it might be changed by server in the process of the full re-sync LogUtils.i(Eas.BSK_TAG, "Finding all the local mails with the same timestamp"); StringBuilder timestampList = new StringBuilder("("); for (Message msg : newEmails) { timestampList.append(msg.mTimeStamp); timestampList.append(','); } // Delete the last comma if (!newEmails.isEmpty()) { timestampList.deleteCharAt(timestampList.length() - 1); } timestampList.append(")"); String selection = MessageColumns.MAILBOX_KEY + "=" + Long.toString(mMailbox.mId) + " AND " + MessageColumns.TIMESTAMP + " IN " + timestampList.toString(); LogUtils.i(Eas.BSK_TAG, "selection clause: " + selection); Cursor c = mContentResolver.query( Message.CONTENT_URI, new String[] {MessageColumns._ID, MessageColumns.TIMESTAMP}, selection, null, null); try { if (c != null) { final int columnMessageId = 0; final int columnTimestamp = 1; ArrayList<ContentProviderOperation> bskOps = new ArrayList<ContentProviderOperation>(); LogUtils.i( Eas.BSK_TAG, "For current window, found " + c.getCount() + " mails existed in local DB"); while (c.moveToNext()) { long timestamp = c.getLong(columnTimestamp); for (Message msg : newEmails) { if (msg.mTimeStamp == timestamp) { // Update the properties of local mails ContentValues cv = new ContentValues(); cv.put(MessageColumns.SERVER_ID, msg.mServerId); cv.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); cv.put(MessageColumns.FLAGS, msg.mFlags); cv.put(MessageColumns.FLAG_READ, msg.mFlagRead); cv.put(MessageColumns.DIRTY, "0"); bskOps.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId( Message.CONTENT_URI, c.getInt(columnMessageId))) .withValues(cv) .build()); // Remove the existed mail from new mail list, then it would not be store again newEmails.remove(msg); applyBatchIfNeeded(bskOps, maxOpsPerBatch, false); break; } } } applyBatchIfNeeded(bskOps, maxOpsPerBatch, true); } } catch (RemoteException e) { // There is nothing to be done here; fail by returning null LogUtils.i(Eas.BSK_TAG, "RemoteException when applyBatch"); } catch (OperationApplicationException e) { // There is nothing to be done here; fail by returning null LogUtils.i(Eas.BSK_TAG, "OperationApplicationException when applyBatch"); } finally { if (c != null) { c.close(); } } LogUtils.i( Eas.BSK_TAG, "There are " + newEmails.size() + " mail(s) remaining in the newEmails list"); } /** @} */ /// M: For smart push, record new mails' coming DataCollectUtils.recordNewMails(mContext, newEmails); for (EmailContent.Message msg : newEmails) { msg.addSaveOps(ops); applyBatchIfNeeded(ops, maxOpsPerBatch, false); } /// M: Log receive new message. @{ EmailContent.Message.logMessageReceived(mContext, newEmails.toArray(new Message[] {})); /// @} for (Long id : deletedEmails) { /** * M: 1. The attachments were belonged to the com.android.email application, delete * attachments here, it would fail, so mask the function call to * AttachmentUtilities.deleteAllAttachmentFiles here and do the attachment delete operation on * Email provider. 2. Add a new parameter (account id) to URI for delete attachments on Email * side. @{ */ Uri.Builder builder = EmailContent.Message.CONTENT_URI.buildUpon(); builder.appendEncodedPath(String.valueOf(id)); builder.appendQueryParameter( AttachmentUtilities.KEY_ACCOUNT_ID, String.valueOf(mAccount.mId)); ops.add(ContentProviderOperation.newDelete(builder.build()).build()); // AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id); /** @} */ applyBatchIfNeeded(ops, maxOpsPerBatch, false); } if (!changedEmails.isEmpty()) { // Server wins in a conflict... for (ServerChange change : changedEmails) { ContentValues cv = new ContentValues(); if (change.read != null) { cv.put(EmailContent.MessageColumns.FLAG_READ, change.read); } if (change.flag != null) { cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag); } if (change.flags != null) { cv.put(EmailContent.MessageColumns.FLAGS, change.flags); } ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id)) .withValues(cv) .build()); } applyBatchIfNeeded(ops, maxOpsPerBatch, false); } // We only want to update the sync key here ContentValues mailboxValues = new ContentValues(); mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey); ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)) .withValues(mailboxValues) .build()); applyBatchIfNeeded(ops, maxOpsPerBatch, true); userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey); }
/// M: The folder sync must be synchronized, otherwise the folders may be duplicate @Override public synchronized void updateFolderList(long accountId) throws RemoteException { /// M: We Can't updateFolderList in low storage state @{ if (StorageLowState.checkIfStorageLow(mContext)) { LogUtils.e(Logging.LOG_TAG, "Can't updateFolderList due to low storage"); return; } /// @} final Account account = Account.restoreAccountWithId(mContext, accountId); if (account == null) { return; } long inboxId = -1; TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); Cursor localFolderCursor = null; try { // Step 0: Make sure the default system mailboxes exist. for (final int type : Mailbox.REQUIRED_FOLDER_TYPES) { if (Mailbox.findMailboxOfType(mContext, accountId, type) == Mailbox.NO_MAILBOX) { final Mailbox mailbox = Mailbox.newSystemMailbox(mContext, accountId, type); mailbox.save(mContext); if (type == Mailbox.TYPE_INBOX) { inboxId = mailbox.mId; } } } // Step 1: Get remote mailboxes final Store store = Store.getInstance(account, mContext); final Folder[] remoteFolders = store.updateFolders(); final HashSet<String> remoteFolderNames = new HashSet<String>(); for (final Folder remoteFolder : remoteFolders) { remoteFolderNames.add(remoteFolder.getName()); } // Step 2: Get local mailboxes localFolderCursor = mContext .getContentResolver() .query( Mailbox.CONTENT_URI, MAILBOX_PROJECTION, EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", new String[] {String.valueOf(account.mId)}, null); // Step 3: Remove any local mailbox not on the remote list while (localFolderCursor.moveToNext()) { final String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); // Short circuit if we have a remote mailbox with the same name if (remoteFolderNames.contains(mailboxPath)) { continue; } final int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); final long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); switch (mailboxType) { case Mailbox.TYPE_INBOX: case Mailbox.TYPE_DRAFTS: case Mailbox.TYPE_OUTBOX: case Mailbox.TYPE_SENT: case Mailbox.TYPE_TRASH: case Mailbox.TYPE_SEARCH: // Never, ever delete special mailboxes break; default: // Drop all attachment files related to this mailbox AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, accountId, mailboxId); // Delete the mailbox; database triggers take care of related // Message, Body and Attachment records Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId); mContext.getContentResolver().delete(uri, null, null); break; } } } catch (MessagingException me) { LogUtils.i(Logging.LOG_TAG, me, "Error in updateFolderList"); // We'll hope this is temporary } finally { if (localFolderCursor != null) { localFolderCursor.close(); } // If we just created the inbox, sync it if (inboxId != -1) { startSync(inboxId, true, 0); } } }
@Override public void loadAttachment( final IEmailServiceCallback cb, final long attachmentId, final boolean background) throws RemoteException { /// M: We Can't load attachment in low storage state @{ if (StorageLowState.checkIfStorageLow(mContext)) { LogUtils.e(Logging.LOG_TAG, "Can't load attachment due to low storage"); cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } /// @} Folder remoteFolder = null; try { // 1. Check if the attachment is already here and return early in that case Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); if (attachment == null) { cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); return; } final long messageId = attachment.mMessageKey; final EmailContent.Message message = EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); if (message == null) { cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.MESSAGE_NOT_FOUND, 0); return; } // If the message is loaded, just report that we're finished if (Utility.attachmentExists(mContext, attachment) && attachment.mUiState == UIProvider.AttachmentState.SAVED) { cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } // Say we're starting... cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0); // 2. Open the remote folder. final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); if (mailbox.mType == Mailbox.TYPE_OUTBOX /// M: View an attachment which comes from refMessage need sourceKey to identify || mailbox.mType == Mailbox.TYPE_DRAFTS) { long sourceId = Utility.getFirstRowLong( mContext, Body.CONTENT_URI, new String[] {BodyColumns.SOURCE_MESSAGE_KEY}, BodyColumns.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null, 0, -1L); if (sourceId != -1) { EmailContent.Message sourceMsg = EmailContent.Message.restoreMessageWithId(mContext, sourceId); if (sourceMsg != null) { mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey); message.mServerId = sourceMsg.mServerId; } } } else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) { mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey); } if (account == null || mailbox == null) { // If the account/mailbox are gone, just report success; the UI handles this cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } TrafficStats.setThreadStatsTag(TrafficFlags.getAttachmentFlags(mContext, account)); final Store remoteStore = Store.getInstance(account, mContext); remoteFolder = remoteStore.getFolder(mailbox.mServerId); remoteFolder.open(OpenMode.READ_WRITE); // 3. Generate a shell message in which to retrieve the attachment, // and a shell BodyPart for the attachment. Then glue them together. final Message storeMessage = remoteFolder.createMessage(message.mServerId); final MimeBodyPart storePart = new MimeBodyPart(); storePart.setSize((int) attachment.mSize); storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, attachment.mLocation); storePart.setHeader( MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", attachment.mMimeType, attachment.mFileName)); // TODO is this always true for attachments? I think we dropped the // true encoding along the way /// M: set encoding type according to data base record. String encoding = attachment.mEncoding; if (TextUtils.isEmpty(encoding)) { encoding = "base64"; } storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); final MimeMultipart multipart = new MimeMultipart(); multipart.setSubType("mixed"); multipart.addBodyPart(storePart); storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); storeMessage.setBody(multipart); // 4. Now ask for the attachment to be fetched final FetchProfile fp = new FetchProfile(); fp.add(storePart); remoteFolder.fetch( new Message[] {storeMessage}, fp, new MessageRetrievalListenerBridge(messageId, attachmentId, cb)); // If we failed to load the attachment, throw an Exception here, so that // AttachmentDownloadService knows that we failed if (storePart.getBody() == null) { throw new MessagingException("Attachment not loaded."); } // Save the attachment to wherever it's going AttachmentUtilities.saveAttachment( mContext, storePart.getBody().getInputStream(), attachment); // 6. Report success cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); } catch (MessagingException me) { LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment"); final ContentValues cv = new ContentValues(1); cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED); final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); mContext.getContentResolver().update(uri, cv, null, null); cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0); } finally { if (remoteFolder != null) { remoteFolder.close(false); } } }