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
This commit is contained in:
Alon Albert 2013-10-17 17:46:26 -07:00
parent c084e8188d
commit 8c989772df
4 changed files with 213 additions and 5 deletions

View File

@ -60,6 +60,7 @@ public abstract class EmailServiceStatus {
public static final String SYNC_STATUS_TYPE = "type"; public static final String SYNC_STATUS_TYPE = "type";
public static final String SYNC_STATUS_ID = "id"; public static final String SYNC_STATUS_ID = "id";
public static final String SYNC_STATUS_CODE = "status_code"; public static final String SYNC_STATUS_CODE = "status_code";
public static final String SYNC_RESULT = "result";
public static final String SYNC_STATUS_PROGRESS = "progress"; public static final String SYNC_STATUS_PROGRESS = "progress";
// Values for the SYNC_STATUS_TYPE to specify what kind of sync status we're returning. // 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, private static void syncStatus(final ContentResolver cr, final Bundle syncExtras,
final int statusType, final long id, final int statusCode, final int progress, final int statusType, final long id, final int statusCode, final int progress,
int syncResult,
final StatusWriter writer) { final StatusWriter writer) {
final String callbackUri = syncExtras.getString(SYNC_EXTRAS_CALLBACK_URI); final String callbackUri = syncExtras.getString(SYNC_EXTRAS_CALLBACK_URI);
final String callbackMethod = syncExtras.getString(SYNC_EXTRAS_CALLBACK_METHOD); final String callbackMethod = syncExtras.getString(SYNC_EXTRAS_CALLBACK_METHOD);
@ -97,6 +99,9 @@ public abstract class EmailServiceStatus {
statusExtras.putInt(SYNC_STATUS_TYPE, statusType); statusExtras.putInt(SYNC_STATUS_TYPE, statusType);
statusExtras.putLong(SYNC_STATUS_ID, id); statusExtras.putLong(SYNC_STATUS_ID, id);
statusExtras.putInt(SYNC_STATUS_CODE, statusCode); statusExtras.putInt(SYNC_STATUS_CODE, statusCode);
if (statusCode != IN_PROGRESS) {
statusExtras.putInt(SYNC_RESULT, syncResult);
}
statusExtras.putInt(SYNC_STATUS_PROGRESS, progress); statusExtras.putInt(SYNC_STATUS_PROGRESS, progress);
if (writer != null) { if (writer != null) {
writer.addToStatus(statusExtras); writer.addToStatus(statusExtras);
@ -116,8 +121,9 @@ public abstract class EmailServiceStatus {
* @param progress The progress of this sync operation. * @param progress The progress of this sync operation.
*/ */
public static void syncMailboxStatus(final ContentResolver cr, final Bundle syncExtras, public static void syncMailboxStatus(final ContentResolver cr, final Bundle syncExtras,
final long mailboxId, final int statusCode, final int progress) { final long mailboxId, final int statusCode, final int progress, int syncResult) {
syncStatus(cr, syncExtras, SYNC_STATUS_TYPE_MAILBOX, mailboxId, statusCode, progress, null); syncStatus(cr, syncExtras, SYNC_STATUS_TYPE_MAILBOX, mailboxId, statusCode, progress,
syncResult, null);
} }
} }

View File

@ -1911,8 +1911,22 @@ public class EmailProvider extends ContentProvider {
private void updateSyncStatus(final Bundle extras) { private void updateSyncStatus(final Bundle extras) {
final long id = extras.getLong(EmailServiceStatus.SYNC_STATUS_ID); 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); final Uri uri = ContentUris.withAppendedId(FOLDER_STATUS_URI, id);
EmailProvider.this.getContext().getContentResolver().notifyChange(uri, null); 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 @Override
@ -5178,6 +5192,26 @@ public class EmailProvider extends ContentProvider {
private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) { private Cursor uiFolderRefresh(final Mailbox mailbox, final int deltaMessageCount) {
if (mailbox != null) { 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); startSync(mailbox, deltaMessageCount);
} }
return null; return null;

View File

@ -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<Long, Boolean> mMailboxSync = new HashMap<Long, Boolean>();
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);
}
}

View File

@ -40,6 +40,7 @@ import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.EmailServiceStatus;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils; import com.android.mail.utils.LogUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -130,7 +131,7 @@ public class PopImapSyncAdapterService extends Service {
EmailServiceStub.sendMailImpl(context, account.mId); EmailServiceStub.sendMailImpl(context, account.mId);
} else { } else {
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId,
EmailServiceStatus.IN_PROGRESS, 0); EmailServiceStatus.IN_PROGRESS, 0, UIProvider.LastSyncResult.SUCCESS);
final int status; final int status;
if (protocol.equals(legacyImapProtocol)) { if (protocol.equals(legacyImapProtocol)) {
status = ImapService.synchronizeMailboxSynchronous(context, account, status = ImapService.synchronizeMailboxSynchronous(context, account,
@ -139,20 +140,28 @@ public class PopImapSyncAdapterService extends Service {
status = Pop3Service.synchronizeMailboxSynchronous(context, account, status = Pop3Service.synchronizeMailboxSynchronous(context, account,
mailbox, deltaMessageCount); mailbox, deltaMessageCount);
} }
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0); EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, status, 0,
UIProvider.LastSyncResult.SUCCESS);
} }
} catch (MessagingException e) { } catch (MessagingException e) {
int cause = e.getExceptionType(); int cause = e.getExceptionType();
// XXX It's no good to put the MessagingException.cause here, that's not the // XXX It's no good to put the MessagingException.cause here, that's not the
// same set of values that we use in EmailServiceStatus. // same set of values that we use in EmailServiceStatus.
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0);
switch(cause) { switch(cause) {
case MessagingException.IOERROR: case MessagingException.IOERROR:
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0,
UIProvider.LastSyncResult.CONNECTION_ERROR);
syncResult.stats.numIoExceptions++; syncResult.stats.numIoExceptions++;
break; break;
case MessagingException.AUTHENTICATION_FAILED: case MessagingException.AUTHENTICATION_FAILED:
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0,
UIProvider.LastSyncResult.AUTH_ERROR);
syncResult.stats.numAuthExceptions++; syncResult.stats.numAuthExceptions++;
break; break;
default:
EmailServiceStatus.syncMailboxStatus(resolver, extras, mailboxId, cause, 0,
UIProvider.LastSyncResult.INTERNAL_ERROR);
} }
} }
} finally { } finally {