package com.fsck.k9.provider; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import android.annotation.TargetApi; import android.app.Application; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.CrossProcessCursor; import android.database.Cursor; import android.database.CursorWindow; import android.database.DataSetObserver; import android.database.MatrixCursor; import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.provider.BaseColumns; import timber.log.Timber; import com.fsck.k9.Account; import com.fsck.k9.AccountStats; import com.fsck.k9.BuildConfig; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.activity.FolderInfoHolder; import com.fsck.k9.activity.MessageInfoHolder; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.SimpleMessagingListener; import com.fsck.k9.helper.MessageHelper; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mailstore.LocalFolder; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.search.SearchAccount; public class MessageProvider extends ContentProvider { public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".messageprovider"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); private static final String[] DEFAULT_MESSAGE_PROJECTION = new String[] { MessageColumns._ID, MessageColumns.SEND_DATE, MessageColumns.SENDER, MessageColumns.SUBJECT, MessageColumns.PREVIEW, MessageColumns.ACCOUNT, MessageColumns.URI, MessageColumns.DELETE_URI, MessageColumns.SENDER_ADDRESS }; private static final String[] DEFAULT_ACCOUNT_PROJECTION = new String[] { AccountColumns.ACCOUNT_NUMBER, AccountColumns.ACCOUNT_NAME, }; private static final String[] UNREAD_PROJECTION = new String[] { UnreadColumns.ACCOUNT_NAME, UnreadColumns.UNREAD }; private UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); private List<QueryHandler> queryHandlers = new ArrayList<QueryHandler>(); private MessageHelper messageHelper; /** * How many simultaneous cursors we can afford to expose at once */ Semaphore semaphore = new Semaphore(1); ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(1); @Override public boolean onCreate() { messageHelper = MessageHelper.getInstance(getContext()); registerQueryHandler(new ThrottlingQueryHandler(new AccountsQueryHandler())); registerQueryHandler(new ThrottlingQueryHandler(new MessagesQueryHandler())); registerQueryHandler(new ThrottlingQueryHandler(new UnreadQueryHandler())); K9.registerApplicationAware(new K9.ApplicationAware() { @Override public void initializeComponent(final Application application) { Timber.v("Registering content resolver notifier"); MessagingController.getInstance(application).addListener(new SimpleMessagingListener() { @Override public void folderStatusChanged(Account account, String folderName, int unreadMessageCount) { application.getContentResolver().notifyChange(CONTENT_URI, null); } }); } }); return true; } @Override public String getType(Uri uri) { if (K9.app == null) { return null; } Timber.v("MessageProvider/getType: %s", uri); return null; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (K9.app == null) { return null; } Timber.v("MessageProvider/query: %s", uri); int code = uriMatcher.match(uri); if (code == -1) { throw new IllegalStateException("Unrecognized URI: " + uri); } Cursor cursor; try { QueryHandler handler = queryHandlers.get(code); cursor = handler.query(uri, projection, selection, selectionArgs, sortOrder); } catch (Exception e) { Timber.e(e, "Unable to execute query for URI: %s", uri); return null; } return cursor; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { if (K9.app == null) { return 0; } Timber.v("MessageProvider/delete: %s", uri); // Note: can only delete a message List<String> segments = uri.getPathSegments(); int accountId = Integer.parseInt(segments.get(1)); String folderName = segments.get(2); String msgUid = segments.get(3); // get account Account myAccount = null; for (Account account : Preferences.getPreferences(getContext()).getAccounts()) { if (account.getAccountNumber() == accountId) { myAccount = account; if (!account.isAvailable(getContext())) { Timber.w("not deleting messages because account is unavailable at the moment"); return 0; } } } if (myAccount == null) { Timber.e("Could not find account with id %d", accountId); } if (myAccount != null) { MessageReference messageReference = new MessageReference(myAccount.getUuid(), folderName, msgUid, null); MessagingController controller = MessagingController.getInstance(getContext()); controller.deleteMessage(messageReference, null); } // FIXME return the actual number of deleted messages return 0; } @Override public Uri insert(Uri uri, ContentValues values) { if (K9.app == null) { return null; } Timber.v("MessageProvider/insert: %s", uri); return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (K9.app == null) { return 0; } Timber.v("MessageProvider/update: %s", uri); // TBD return 0; } /** * Register a {@link QueryHandler} to handle a certain {@link Uri} for * {@link #query(Uri, String[], String, String[], String)} */ protected void registerQueryHandler(QueryHandler handler) { if (queryHandlers.contains(handler)) { return; } queryHandlers.add(handler); int code = queryHandlers.indexOf(handler); uriMatcher.addURI(AUTHORITY, handler.getPath(), code); } public static class ReverseDateComparator implements Comparator<MessageInfoHolder> { @Override public int compare(MessageInfoHolder object2, MessageInfoHolder object1) { if (object1.compareDate == null) { return (object2.compareDate == null ? 0 : 1); } else if (object2.compareDate == null) { return -1; } else { return object1.compareDate.compareTo(object2.compareDate); } } } public interface MessageColumns extends BaseColumns { /** * The number of milliseconds since Jan. 1, 1970, midnight GMT. * * <P>Type: INTEGER (long)</P> */ String SEND_DATE = "date"; /** * <P>Type: TEXT</P> */ String SENDER = "sender"; /** * <P>Type: TEXT</P> */ String SENDER_ADDRESS = "senderAddress"; /** * <P>Type: TEXT</P> */ String SUBJECT = "subject"; /** * <P>Type: TEXT</P> */ String PREVIEW = "preview"; /** * <P>Type: BOOLEAN</P> */ String UNREAD = "unread"; /** * <P>Type: TEXT</P> */ String ACCOUNT = "account"; /** * <P>Type: INTEGER</P> */ String ACCOUNT_NUMBER = "accountNumber"; /** * <P>Type: BOOLEAN</P> */ String HAS_ATTACHMENTS = "hasAttachments"; /** * <P>Type: BOOLEAN</P> */ String HAS_STAR = "hasStar"; /** * <P>Type: INTEGER</P> */ String ACCOUNT_COLOR = "accountColor"; String URI = "uri"; String DELETE_URI = "delUri"; /** * @deprecated the field value is misnamed/misleading - present for compatibility purpose only. To be removed. */ @Deprecated String INCREMENT = "id"; } public interface AccountColumns { /** * <P>Type: INTEGER</P> */ String ACCOUNT_NUMBER = "accountNumber"; /** * <P>Type: String</P> */ String ACCOUNT_NAME = "accountName"; String ACCOUNT_UUID = "accountUuid"; String ACCOUNT_COLOR = "accountColor"; } public interface UnreadColumns { /** * <P>Type: String</P> */ String ACCOUNT_NAME = "accountName"; /** * <P>Type: INTEGER</P> */ String UNREAD = "unread"; } protected interface QueryHandler { /** * The path this instance is able to respond to. */ String getPath(); Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws Exception; } /** * Extracts a value from an object. */ public interface FieldExtractor<T, K> { K getField(T source); } /** * Extracts the {@link LocalMessage#getId() ID} from the given {@link MessageInfoHolder}. The underlying * {@link Message} is expected to be a {@link LocalMessage}. */ public static class IdExtractor implements FieldExtractor<MessageInfoHolder, Long> { @Override public Long getField(MessageInfoHolder source) { return source.message.getId(); } } public static class CountExtractor<T> implements FieldExtractor<T, Integer> { private Integer count; public CountExtractor(int count) { this.count = count; } @Override public Integer getField(T source) { return count; } } public static class SubjectExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { return source.message.getSubject(); } } public static class SendDateExtractor implements FieldExtractor<MessageInfoHolder, Long> { @Override public Long getField(MessageInfoHolder source) { return source.message.getSentDate().getTime(); } } public static class PreviewExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { return source.message.getPreview(); } } public static class UriExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { return source.uri; } } public static class DeleteUriExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { LocalMessage message = source.message; int accountNumber = message.getAccount().getAccountNumber(); return CONTENT_URI.buildUpon() .appendPath("delete_message") .appendPath(Integer.toString(accountNumber)) .appendPath(message.getFolder().getName()) .appendPath(message.getUid()) .build() .toString(); } } public static class SenderExtractor implements FieldExtractor<MessageInfoHolder, CharSequence> { @Override public CharSequence getField(MessageInfoHolder source) { return source.sender; } } public static class SenderAddressExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { return source.senderAddress; } } public static class AccountExtractor implements FieldExtractor<MessageInfoHolder, String> { @Override public String getField(MessageInfoHolder source) { return source.message.getAccount().getDescription(); } } public static class AccountColorExtractor implements FieldExtractor<MessageInfoHolder, Integer> { @Override public Integer getField(MessageInfoHolder source) { return source.message.getAccount().getChipColor(); } } public static class AccountNumberExtractor implements FieldExtractor<MessageInfoHolder, Integer> { @Override public Integer getField(MessageInfoHolder source) { return source.message.getAccount().getAccountNumber(); } } public static class HasAttachmentsExtractor implements FieldExtractor<MessageInfoHolder, Boolean> { @Override public Boolean getField(MessageInfoHolder source) { return source.message.hasAttachments(); } } public static class HasStarExtractor implements FieldExtractor<MessageInfoHolder, Boolean> { @Override public Boolean getField(MessageInfoHolder source) { return source.message.isSet(Flag.FLAGGED); } } public static class UnreadExtractor implements FieldExtractor<MessageInfoHolder, Boolean> { @Override public Boolean getField(MessageInfoHolder source) { return !source.read; } } /** * @deprecated having an incremental value has no real interest, implemented for compatibility only */ @Deprecated public static class IncrementExtractor implements FieldExtractor<MessageInfoHolder, Integer> { private int count = 0; @Override public Integer getField(MessageInfoHolder source) { return count++; } } /** * Retrieve messages from the integrated inbox. */ protected class MessagesQueryHandler implements QueryHandler { @Override public String getPath() { return "inbox_messages/"; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws Exception { return getMessages(projection); } protected MatrixCursor getMessages(String[] projection) throws InterruptedException { BlockingQueue<List<MessageInfoHolder>> queue = new SynchronousQueue<List<MessageInfoHolder>>(); // new code for integrated inbox, only execute this once as it will be processed afterwards via the listener SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(getContext()); MessagingController msgController = MessagingController.getInstance(getContext()); msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch(), new MessageInfoHolderRetrieverListener(queue)); List<MessageInfoHolder> holders = queue.take(); // TODO add sort order parameter Collections.sort(holders, new ReverseDateComparator()); String[] projectionToUse; if (projection == null) { projectionToUse = DEFAULT_MESSAGE_PROJECTION; } else { projectionToUse = projection; } LinkedHashMap<String, FieldExtractor<MessageInfoHolder, ?>> extractors = resolveMessageExtractors(projectionToUse, holders.size()); int fieldCount = extractors.size(); String[] actualProjection = extractors.keySet().toArray(new String[fieldCount]); MatrixCursor cursor = new MatrixCursor(actualProjection); for (MessageInfoHolder holder : holders) { Object[] o = new Object[fieldCount]; int i = 0; for (FieldExtractor<MessageInfoHolder, ?> extractor : extractors.values()) { o[i] = extractor.getField(holder); i += 1; } cursor.addRow(o); } return cursor; } protected LinkedHashMap<String, FieldExtractor<MessageInfoHolder, ?>> resolveMessageExtractors( String[] projection, int count) { LinkedHashMap<String, FieldExtractor<MessageInfoHolder, ?>> extractors = new LinkedHashMap<String, FieldExtractor<MessageInfoHolder, ?>>(); for (String field : projection) { if (extractors.containsKey(field)) { continue; } if (MessageColumns._ID.equals(field)) { extractors.put(field, new IdExtractor()); } else if (MessageColumns._COUNT.equals(field)) { extractors.put(field, new CountExtractor<MessageInfoHolder>(count)); } else if (MessageColumns.SUBJECT.equals(field)) { extractors.put(field, new SubjectExtractor()); } else if (MessageColumns.SENDER.equals(field)) { extractors.put(field, new SenderExtractor()); } else if (MessageColumns.SENDER_ADDRESS.equals(field)) { extractors.put(field, new SenderAddressExtractor()); } else if (MessageColumns.SEND_DATE.equals(field)) { extractors.put(field, new SendDateExtractor()); } else if (MessageColumns.PREVIEW.equals(field)) { extractors.put(field, new PreviewExtractor()); } else if (MessageColumns.URI.equals(field)) { extractors.put(field, new UriExtractor()); } else if (MessageColumns.DELETE_URI.equals(field)) { extractors.put(field, new DeleteUriExtractor()); } else if (MessageColumns.UNREAD.equals(field)) { extractors.put(field, new UnreadExtractor()); } else if (MessageColumns.ACCOUNT.equals(field)) { extractors.put(field, new AccountExtractor()); } else if (MessageColumns.ACCOUNT_COLOR.equals(field)) { extractors.put(field, new AccountColorExtractor()); } else if (MessageColumns.ACCOUNT_NUMBER.equals(field)) { extractors.put(field, new AccountNumberExtractor()); } else if (MessageColumns.HAS_ATTACHMENTS.equals(field)) { extractors.put(field, new HasAttachmentsExtractor()); } else if (MessageColumns.HAS_STAR.equals(field)) { extractors.put(field, new HasStarExtractor()); } else if (MessageColumns.INCREMENT.equals(field)) { extractors.put(field, new IncrementExtractor()); } } return extractors; } } /** * Retrieve the account list. */ protected class AccountsQueryHandler implements QueryHandler { @Override public String getPath() { return "accounts"; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws Exception { return getAllAccounts(projection); } public Cursor getAllAccounts(String[] projection) { if (projection == null) { projection = DEFAULT_ACCOUNT_PROJECTION; } MatrixCursor cursor = new MatrixCursor(projection); for (Account account : Preferences.getPreferences(getContext()).getAccounts()) { Object[] values = new Object[projection.length]; int fieldIndex = 0; for (String field : projection) { if (AccountColumns.ACCOUNT_NUMBER.equals(field)) { values[fieldIndex] = account.getAccountNumber(); } else if (AccountColumns.ACCOUNT_NAME.equals(field)) { values[fieldIndex] = account.getDescription(); } else if (AccountColumns.ACCOUNT_UUID.equals(field)) { values[fieldIndex] = account.getUuid(); } else if (AccountColumns.ACCOUNT_COLOR.equals(field)) { values[fieldIndex] = account.getChipColor(); } else { values[fieldIndex] = null; } ++fieldIndex; } cursor.addRow(values); } return cursor; } } /** * Retrieve the unread message count for a given account specified by its {@link Account#getAccountNumber() number}. */ protected class UnreadQueryHandler implements QueryHandler { @Override public String getPath() { return "account_unread/#"; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws Exception { List<String> segments = uri.getPathSegments(); int accountId = Integer.parseInt(segments.get(1)); /* * This method below calls Account.getStats() which uses EmailProvider to do its work. * For this to work we need to clear the calling identity. Otherwise accessing * EmailProvider will fail because it's not exported so third-party apps can't access it * directly. */ long identityToken = Binder.clearCallingIdentity(); try { return getAccountUnread(accountId); } finally { Binder.restoreCallingIdentity(identityToken); } } private Cursor getAccountUnread(int accountNumber) { MatrixCursor cursor = new MatrixCursor(UNREAD_PROJECTION); Account myAccount; AccountStats myAccountStats; Object[] values = new Object[2]; for (Account account : Preferences.getPreferences(getContext()).getAvailableAccounts()) { if (account.getAccountNumber() == accountNumber) { myAccount = account; try { myAccountStats = account.getStats(getContext()); values[0] = myAccount.getDescription(); if (myAccountStats == null) { values[1] = 0; } else { values[1] = myAccountStats.unreadMessageCount; } } catch (MessagingException e) { Timber.e(e.getMessage()); values[0] = "Unknown"; values[1] = 0; } cursor.addRow(values); } } return cursor; } } /** * Cursor wrapper that release a semaphore on close. Close is also triggered on {@link #finalize()}. */ protected static class MonitoredCursor implements CrossProcessCursor { /** * The underlying cursor implementation that handles regular requests */ private CrossProcessCursor cursor; /** * Whether {@link #close()} was invoked */ private AtomicBoolean closed = new AtomicBoolean(false); private Semaphore semaphore; protected MonitoredCursor(CrossProcessCursor cursor, Semaphore semaphore) { this.cursor = cursor; this.semaphore = semaphore; } @Override public void close() { if (closed.compareAndSet(false, true)) { cursor.close(); Timber.d("Cursor closed, null'ing & releasing semaphore"); cursor = null; semaphore.release(); } } @Override public boolean isClosed() { return closed.get() || cursor.isClosed(); } @Override protected void finalize() throws Throwable { close(); super.finalize(); } protected void checkClosed() throws IllegalStateException { if (closed.get()) { throw new IllegalStateException("Cursor was closed"); } } @Override public void fillWindow(int pos, CursorWindow winow) { checkClosed(); cursor.fillWindow(pos, winow); } @Override public CursorWindow getWindow() { checkClosed(); return cursor.getWindow(); } @Override public boolean onMove(int oldPosition, int newPosition) { checkClosed(); return cursor.onMove(oldPosition, newPosition); } @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { checkClosed(); cursor.copyStringToBuffer(columnIndex, buffer); } @Override public void deactivate() { checkClosed(); cursor.deactivate(); } @Override public byte[] getBlob(int columnIndex) { checkClosed(); return cursor.getBlob(columnIndex); } @Override public int getColumnCount() { checkClosed(); return cursor.getColumnCount(); } @Override public int getColumnIndex(String columnName) { checkClosed(); return cursor.getColumnIndex(columnName); } @Override public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { checkClosed(); return cursor.getColumnIndexOrThrow(columnName); } @Override public String getColumnName(int columnIndex) { checkClosed(); return cursor.getColumnName(columnIndex); } @Override public String[] getColumnNames() { checkClosed(); return cursor.getColumnNames(); } @Override public int getCount() { checkClosed(); return cursor.getCount(); } @Override public double getDouble(int columnIndex) { checkClosed(); return cursor.getDouble(columnIndex); } @Override public Bundle getExtras() { checkClosed(); return cursor.getExtras(); } @Override public float getFloat(int columnIndex) { checkClosed(); return cursor.getFloat(columnIndex); } @Override public int getInt(int columnIndex) { checkClosed(); return cursor.getInt(columnIndex); } @Override public long getLong(int columnIndex) { checkClosed(); return cursor.getLong(columnIndex); } @Override public int getPosition() { checkClosed(); return cursor.getPosition(); } @Override public short getShort(int columnIndex) { checkClosed(); return cursor.getShort(columnIndex); } @Override public String getString(int columnIndex) { checkClosed(); return cursor.getString(columnIndex); } @Override public boolean getWantsAllOnMoveCalls() { checkClosed(); return cursor.getWantsAllOnMoveCalls(); } @TargetApi(Build.VERSION_CODES.M) @Override public void setExtras(Bundle extras) { cursor.setExtras(extras); } @Override public boolean isAfterLast() { checkClosed(); return cursor.isAfterLast(); } @Override public boolean isBeforeFirst() { checkClosed(); return cursor.isBeforeFirst(); } @Override public boolean isFirst() { checkClosed(); return cursor.isFirst(); } @Override public boolean isLast() { checkClosed(); return cursor.isLast(); } @Override public boolean isNull(int columnIndex) { checkClosed(); return cursor.isNull(columnIndex); } @Override public boolean move(int offset) { checkClosed(); return cursor.move(offset); } @Override public boolean moveToFirst() { checkClosed(); return cursor.moveToFirst(); } @Override public boolean moveToLast() { checkClosed(); return cursor.moveToLast(); } @Override public boolean moveToNext() { checkClosed(); return cursor.moveToNext(); } @Override public boolean moveToPosition(int position) { checkClosed(); return cursor.moveToPosition(position); } @Override public boolean moveToPrevious() { checkClosed(); return cursor.moveToPrevious(); } @Override public void registerContentObserver(ContentObserver observer) { checkClosed(); cursor.registerContentObserver(observer); } @Override public void registerDataSetObserver(DataSetObserver observer) { checkClosed(); cursor.registerDataSetObserver(observer); } @SuppressWarnings("deprecation") @Override public boolean requery() { checkClosed(); return cursor.requery(); } @Override public Bundle respond(Bundle extras) { checkClosed(); return cursor.respond(extras); } @Override public void setNotificationUri(ContentResolver cr, Uri uri) { checkClosed(); cursor.setNotificationUri(cr, uri); } @Override public void unregisterContentObserver(ContentObserver observer) { checkClosed(); cursor.unregisterContentObserver(observer); } @Override public void unregisterDataSetObserver(DataSetObserver observer) { checkClosed(); cursor.unregisterDataSetObserver(observer); } @Override public int getType(int columnIndex) { checkClosed(); return cursor.getType(columnIndex); } @Override public Uri getNotificationUri() { return null; } } protected class ThrottlingQueryHandler implements QueryHandler { private QueryHandler delegate; public ThrottlingQueryHandler(QueryHandler delegate) { this.delegate = delegate; } @Override public String getPath() { return delegate.getPath(); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws Exception { semaphore.acquire(); Cursor cursor = null; try { cursor = delegate.query(uri, projection, selection, selectionArgs, sortOrder); } finally { if (cursor == null) { semaphore.release(); } } // Android content resolvers can only process CrossProcessCursor instances if (!(cursor instanceof CrossProcessCursor)) { Timber.w("Unsupported cursor, returning null: %s", cursor); semaphore.release(); return null; } MonitoredCursor wrapped = new MonitoredCursor((CrossProcessCursor) cursor, semaphore); // Use a weak reference not to actively prevent garbage collection final WeakReference<MonitoredCursor> weakReference = new WeakReference<MonitoredCursor>(wrapped); // Make sure the cursor is closed after 30 seconds scheduledPool.schedule(new Runnable() { @Override public void run() { MonitoredCursor monitored = weakReference.get(); if (monitored != null && !monitored.isClosed()) { Timber.w("Forcibly closing remotely exposed cursor"); try { monitored.close(); } catch (Exception e) { Timber.w(e, "Exception while forcibly closing cursor"); } } } }, 30, TimeUnit.SECONDS); return wrapped; } } /** * Synchronized listener used to retrieve {@link MessageInfoHolder}s using a given {@link BlockingQueue}. */ protected class MessageInfoHolderRetrieverListener extends SimpleMessagingListener { private final BlockingQueue<List<MessageInfoHolder>> queue; private List<MessageInfoHolder> holders = new ArrayList<MessageInfoHolder>(); public MessageInfoHolderRetrieverListener(BlockingQueue<List<MessageInfoHolder>> queue) { this.queue = queue; } @Override public void listLocalMessagesAddMessages(Account account, String folderName, List<LocalMessage> messages) { Context context = getContext(); for (LocalMessage message : messages) { MessageInfoHolder messageInfoHolder = new MessageInfoHolder(); LocalFolder messageFolder = message.getFolder(); Account messageAccount = message.getAccount(); FolderInfoHolder folderInfoHolder = new FolderInfoHolder(context, messageFolder, messageAccount); messageHelper.populate(messageInfoHolder, message, folderInfoHolder, messageAccount); holders.add(messageInfoHolder); } } @Override public void searchStats(AccountStats stats) { try { queue.put(holders); } catch (InterruptedException e) { Timber.e(e, "Unable to return message list back to caller"); } } } }