From 8c989772dfba08438650575f1ac2bb952bd56158 Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Thu, 17 Oct 2013 17:46:26 -0700 Subject: [PATCH] Handle User Refresh in Edge Cases Handle the following edge cases when a manual refresh is triggered: * No connectivity * Low storage space * Timeout (sync not started) Bug: 11241113 Change-Id: I580235d633fcb65999c0bfe8bf383c9c8ba72110 --- .../service/EmailServiceStatus.java | 10 +- .../android/email/provider/EmailProvider.java | 34 ++++ .../email/provider/RefreshStatusMonitor.java | 159 ++++++++++++++++++ .../service/PopImapSyncAdapterService.java | 15 +- 4 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 src/com/android/email/provider/RefreshStatusMonitor.java diff --git a/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java b/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java index b4f14b38a..553c6d46e 100644 --- a/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java +++ b/emailcommon/src/com/android/emailcommon/service/EmailServiceStatus.java @@ -60,6 +60,7 @@ public abstract class EmailServiceStatus { public static final String SYNC_STATUS_TYPE = "type"; public static final String SYNC_STATUS_ID = "id"; public static final String SYNC_STATUS_CODE = "status_code"; + public static final String SYNC_RESULT = "result"; public static final String SYNC_STATUS_PROGRESS = "progress"; // Values for the SYNC_STATUS_TYPE to specify what kind of sync status we're returning. @@ -88,6 +89,7 @@ public abstract class EmailServiceStatus { */ private static void syncStatus(final ContentResolver cr, final Bundle syncExtras, final int statusType, final long id, final int statusCode, final int progress, + int syncResult, final StatusWriter writer) { final String callbackUri = syncExtras.getString(SYNC_EXTRAS_CALLBACK_URI); final String callbackMethod = syncExtras.getString(SYNC_EXTRAS_CALLBACK_METHOD); @@ -97,6 +99,9 @@ public abstract class EmailServiceStatus { statusExtras.putInt(SYNC_STATUS_TYPE, statusType); statusExtras.putLong(SYNC_STATUS_ID, id); statusExtras.putInt(SYNC_STATUS_CODE, statusCode); + if (statusCode != IN_PROGRESS) { + statusExtras.putInt(SYNC_RESULT, syncResult); + } statusExtras.putInt(SYNC_STATUS_PROGRESS, progress); if (writer != null) { writer.addToStatus(statusExtras); @@ -116,8 +121,9 @@ public abstract class EmailServiceStatus { * @param progress The progress of this sync operation. */ public static void syncMailboxStatus(final ContentResolver cr, final Bundle syncExtras, - final long mailboxId, final int statusCode, final int progress) { - syncStatus(cr, syncExtras, SYNC_STATUS_TYPE_MAILBOX, mailboxId, statusCode, progress, null); + final long mailboxId, final int statusCode, final int progress, int syncResult) { + syncStatus(cr, syncExtras, SYNC_STATUS_TYPE_MAILBOX, mailboxId, statusCode, progress, + syncResult, null); } } diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index a9bdbff5b..c91abab24 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -1911,8 +1911,22 @@ public class EmailProvider extends ContentProvider { private void updateSyncStatus(final Bundle extras) { final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); + final int statusCode = extras.getInt(EmailServiceStatus.SYNC_STATUS_CODE); final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id); EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); + final boolean inProgress = statusCode == EmailServiceStatus.IN_PROGRESS; + if (inProgress) { + RefreshStatusMonitor.getInstance(getContext()).setSyncStarted(id); + } else { + final int result = extras.getInt(EmailServiceStatus.SYNC_RESULT); + final ContentValues values = new ContentValues(); + values.put(Mailbox.UI_LAST_SYNC_RESULT, result); + mDatabase.update( + Mailbox.TABLE_NAME, + values, + WHERE_ID, + new String[] { String.valueOf(id) }); + } } @Override @@ -5178,6 +5192,26 @@ public class EmailProvider extends ContentProvider { private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { if (mailbox != null) { + RefreshStatusMonitor.getInstance(getContext()) + .monitorRefreshStatus(mailbox.mId, new RefreshStatusMonitor.Callback() { + @Override + public void onRefreshCompleted(long mailboxId, int result) { + final ContentValues values = new ContentValues(); + values.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC); + values.put(Mailbox.UI_LAST_SYNC_RESULT, result); + mDatabase.update( + Mailbox.TABLE_NAME, + values, + WHERE_ID, + new String[] { String.valueOf(mailboxId) }); + notifyUIFolder(mailbox.mId, mailbox.mAccountKey); + } + + @Override + public void onTimeout(long mailboxId) { + // todo + } + }); startSync(mailbox, deltaMessageCount); } return null; diff --git a/src/com/android/email/provider/RefreshStatusMonitor.java b/src/com/android/email/provider/RefreshStatusMonitor.java new file mode 100644 index 000000000..604d5c1dc --- /dev/null +++ b/src/com/android/email/provider/RefreshStatusMonitor.java @@ -0,0 +1,159 @@ +package com.android.email.provider; + +import com.android.mail.providers.UIProvider; +import com.android.mail.utils.LogTag; +import com.android.mail.utils.LogUtils; +import com.android.mail.utils.StorageLowState; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.text.format.DateUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class implements a singleton that monitors a mailbox refresh activated by the user. + * The refresh requests a sync but sometimes the sync doesn't happen till much later. This class + * checks if a sync has been started for a specific mailbox. It checks for no network connectivity + * and low storage conditions which prevent a sync and notifies the the caller using a callback. + * If no sync is started after a certain timeout, it gives up and notifies the caller. + */ +public class RefreshStatusMonitor { + private static final String TAG = LogTag.getLogTag(); + + private static final int REMOVE_REFRESH_STATUS_DELAY_MS = 250; + public static final long REMOVE_REFRESH_TIMEOUT_MS = DateUtils.MINUTE_IN_MILLIS; + private static final int MAX_RETRY = + (int) (REMOVE_REFRESH_TIMEOUT_MS / REMOVE_REFRESH_STATUS_DELAY_MS); + + private static RefreshStatusMonitor sInstance = null; + private final Handler mHandler; + private boolean mIsStorageLow = false; + private final Map mMailboxSync = new HashMap(); + + private final Context mContext; + + public static RefreshStatusMonitor getInstance(Context context) { + synchronized (RefreshStatusMonitor.class) { + if (sInstance == null) { + sInstance = new RefreshStatusMonitor(context.getApplicationContext()); + } + } + return sInstance; + } + + private RefreshStatusMonitor(Context context) { + mContext = context; + mHandler = new Handler(mContext.getMainLooper()); + StorageLowState.registerHandler(new StorageLowState + .LowStorageHandler() { + @Override + public void onStorageLow() { + mIsStorageLow = true; + } + + @Override + public void onStorageOk() { + mIsStorageLow = false; + } + }); + } + + public void monitorRefreshStatus(long mailboxId, Callback callback) { + synchronized (mMailboxSync) { + if (!mMailboxSync.containsKey(mailboxId)) + mMailboxSync.put(mailboxId, false); + mHandler.postDelayed( + new RemoveRefreshStatusRunnable(mailboxId, callback), + REMOVE_REFRESH_STATUS_DELAY_MS); + } + } + + public void setSyncStarted(long mailboxId) { + synchronized (mMailboxSync) { + // only if we're tracking this mailbox + if (mMailboxSync.containsKey(mailboxId)) { + LogUtils.d(TAG, "RefreshStatusMonitor: setSyncStarted: mailboxId=%d", mailboxId); + mMailboxSync.put(mailboxId, true); + } + } + } + + private boolean isConnected() { + final ConnectivityManager connectivityManager = + ((ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE)); + final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return (networkInfo != null) && networkInfo.isConnected(); + } + + private class RemoveRefreshStatusRunnable implements Runnable { + private final long mMailboxId; + private final Callback mCallback; + + private int mNumRetries = 0; + + + RemoveRefreshStatusRunnable(long mailboxId, Callback callback) { + mMailboxId = mailboxId; + mCallback = callback; + } + + @Override + public void run() { + synchronized (mMailboxSync) { + final Boolean isSyncRunning = mMailboxSync.get(mMailboxId); + if (Boolean.FALSE.equals(isSyncRunning)) { + if (mIsStorageLow) { + LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d LOW STORAGE", + mMailboxId); + // The device storage is low and sync will never succeed. + mCallback.onRefreshCompleted( + mMailboxId, UIProvider.LastSyncResult.STORAGE_ERROR); + mMailboxSync.remove(mMailboxId); + } else if (!isConnected()) { + LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d NOT CONNECTED", + mMailboxId); + // The device is not connected to the Internet. A sync will never succeed. + mCallback.onRefreshCompleted( + mMailboxId, UIProvider.LastSyncResult.CONNECTION_ERROR); + mMailboxSync.remove(mMailboxId); + } else { + // The device is connected to the Internet. It might take a short while for + // the sync manager to initiate our sync, so let's post this runnable again + // and hope that we have started syncing by then. + mNumRetries++; + LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d Retry %d", + mMailboxId, mNumRetries); + if (mNumRetries > MAX_RETRY) { + LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d TIMEOUT", + mMailboxId); + // Hide the sync status bar if it's been a while since sync was + // requested and still hasn't started. + mMailboxSync.remove(mMailboxId); + mCallback.onTimeout(mMailboxId); + // TODO: Displaying a user friendly message in addition. + } else { + mHandler.postDelayed(this, REMOVE_REFRESH_STATUS_DELAY_MS); + } + } + } else { + // Some sync is currently in progress. We're done + LogUtils.d(TAG, "RefreshStatusMonitor: mailboxId=%d SYNC DETECTED", mMailboxId); + // it's not quite a success yet, the sync just started but we need to clear the + // error so the retry bar goes away. + mCallback.onRefreshCompleted( + mMailboxId, UIProvider.LastSyncResult.SUCCESS); + mMailboxSync.remove(mMailboxId); + } + } + } + } + + public interface Callback { + void onRefreshCompleted(long mailboxId, int result); + void onTimeout(long mailboxId); + } +} diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/PopImapSyncAdapterService.java index 8a95af736..4099be5c2 100644 --- a/src/com/android/email/service/PopImapSyncAdapterService.java +++ b/src/com/android/email/service/PopImapSyncAdapterService.java @@ -40,6 +40,7 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; +import com.android.mail.providers.UIProvider; import com.android.mail.utils.LogUtils; import java.util.ArrayList; @@ -130,7 +131,7 @@ public class PopImapSyncAdapterService extends Service { EmailServiceStub.sendMailImpl(context, account.mId); } else { EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, - EmailServiceStatus.IN_PROGRESS, 0); + EmailServiceStatus.IN_PROGRESS, 0, UIProvider.LastSyncResult.SUCCESS); final int status; if (protocol.equals(legacyImapProtocol)) { status = ImapService.synchronizeMailboxSynchronous(context, account, @@ -139,20 +140,28 @@ public class PopImapSyncAdapterService extends Service { status = Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox, deltaMessageCount); } - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0); + EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0, + UIProvider.LastSyncResult.SUCCESS); } } catch (MessagingException e) { int cause = e.getExceptionType(); // XXX It's no good to put the MessagingException.cause here, that's not the // same set of values that we use in EmailServiceStatus. - EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0); switch(cause) { case MessagingException.IOERROR: + EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0, + UIProvider.LastSyncResult.CONNECTION_ERROR); syncResult.stats.numIoExceptions++; break; case MessagingException.AUTHENTICATION_FAILED: + EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0, + UIProvider.LastSyncResult.AUTH_ERROR); syncResult.stats.numAuthExceptions++; break; + + default: + EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0, + UIProvider.LastSyncResult.INTERNAL_ERROR); } } } finally {