From 45e04b009d570235c542f3c97eaa7e1d00e6cc7b Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Tue, 7 Dec 2010 15:17:52 -0800 Subject: [PATCH] Update error banner 1. Error banner now pushes down the entire screen, rather than covers it. 2. Switch to the new ObjectAnimator for the animation to achieve #1. (Traditional Animation doesn't do this) 3. Dismiss the banner when getting any callback with MessagingException == null and progress > 0, only when the account is the one that caused the last error. 4. MessageListXL now registers its own ControllerResult to detect connection errors, and more importantly, when they're cleared. Bug 3240874 Bug 3240406 Change-Id: I07f8e2f589bb1d312859824f9ec398879003ba16 --- res/anim/header_slide_in.xml | 22 -- res/anim/header_slide_out.xml | 22 -- res/layout/message_list_xl.xml | 20 +- src/com/android/email/Controller.java | 2 +- .../ControllerResultUiThreadWrapper.java | 6 + .../email/activity/BannerController.java | 106 ++++++++++ .../android/email/activity/MessageListXL.java | 197 ++++++++++++------ 7 files changed, 256 insertions(+), 119 deletions(-) delete mode 100644 res/anim/header_slide_in.xml delete mode 100644 res/anim/header_slide_out.xml create mode 100644 src/com/android/email/activity/BannerController.java diff --git a/res/anim/header_slide_in.xml b/res/anim/header_slide_in.xml deleted file mode 100644 index 926e4dff1..000000000 --- a/res/anim/header_slide_in.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/res/anim/header_slide_out.xml b/res/anim/header_slide_out.xml deleted file mode 100644 index 2bc33c831..000000000 --- a/res/anim/header_slide_out.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/res/layout/message_list_xl.xml b/res/layout/message_list_xl.xml index aba76d37e..ceae29984 100644 --- a/res/layout/message_list_xl.xml +++ b/res/layout/message_list_xl.xml @@ -14,19 +14,13 @@ limitations under the License. --> - - - - + + + diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 5bb365439..5ab0036ab 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -1040,7 +1040,7 @@ public class Controller { public static abstract class Result { private volatile boolean mRegistered; - private void setRegistered(boolean registered) { + protected void setRegistered(boolean registered) { mRegistered = registered; } diff --git a/src/com/android/email/ControllerResultUiThreadWrapper.java b/src/com/android/email/ControllerResultUiThreadWrapper.java index f4f949204..c50e6572a 100644 --- a/src/com/android/email/ControllerResultUiThreadWrapper.java +++ b/src/com/android/email/ControllerResultUiThreadWrapper.java @@ -41,6 +41,12 @@ public class ControllerResultUiThreadWrapper extends Result { return mWrappee; } + @Override + protected void setRegistered(boolean registered) { + super.setRegistered(registered); + mWrappee.setRegistered(registered); + } + private void run(Runnable runnable) { if (mHandler == null) { runnable.run(); diff --git a/src/com/android/email/activity/BannerController.java b/src/com/android/email/activity/BannerController.java new file mode 100644 index 000000000..0084091d6 --- /dev/null +++ b/src/com/android/email/activity/BannerController.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email.activity; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.TextView; + +/** + * Class to hide/show a banner. + */ +public class BannerController { + private static final int ANIMATION_DURATION = 100; + private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(1.5f); + + private final TextView mBannerView; + private final int mBannerHeight; + + private boolean mShown; + + /** Hold last animator to cancel. */ + private Animator mLastAnimator; + + public BannerController(Context context, TextView bannerView, int bannerHeight) { + mBannerView = bannerView; + mBannerHeight = bannerHeight; + + setBannerYAnim(-mBannerHeight); // hide by default. + } + + /** + * @return the current y position of the banner. + */ + private int getBannerY() { + return ((ViewGroup.MarginLayoutParams) mBannerView.getLayoutParams()).topMargin; + } + + private static final String PROP_SET_BANNER_Y = "bannerYAnim"; + + /** + * Set the Y position of the banner. public, but should only be used by animators. + */ + public void setBannerYAnim(int y) { + ((ViewGroup.MarginLayoutParams) mBannerView.getLayoutParams()).topMargin = y; + mBannerView.requestLayout(); + } + + /** + * Show a banner with a message. + * + * @return false if a banner is already shown, in which case the message won't be updated. + */ + public boolean show(String message) { + if (mShown) { + return false; // If already shown, don't change the message, to avoid flicker. + } + mShown = true; + mBannerView.setText(message); + slideBanner(0); + return true; + } + + /** + * Dismiss a banner. + */ + public void dismiss() { + if (!mShown) { + return; // Always hidden, or hiding. + } + mShown = false; + slideBanner(-mBannerHeight); // Slide up to hide. + } + + private void slideBanner(int toY) { + if (mLastAnimator != null) { + mLastAnimator.cancel(); + } + + final PropertyValuesHolder[] values = { + PropertyValuesHolder.ofInt(PROP_SET_BANNER_Y, getBannerY(), toY) }; + final ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( + this, values).setDuration(ANIMATION_DURATION); + animator.setInterpolator(INTERPOLATOR); + mLastAnimator = animator; + animator.start(); + } +} diff --git a/src/com/android/email/activity/MessageListXL.java b/src/com/android/email/activity/MessageListXL.java index 87300dd11..0fbe89dba 100644 --- a/src/com/android/email/activity/MessageListXL.java +++ b/src/com/android/email/activity/MessageListXL.java @@ -17,6 +17,8 @@ package com.android.email.activity; import com.android.email.Clock; +import com.android.email.Controller; +import com.android.email.ControllerResultUiThreadWrapper; import com.android.email.Email; import com.android.email.Preferences; import com.android.email.R; @@ -24,8 +26,10 @@ import com.android.email.RefreshManager; import com.android.email.Utility; import com.android.email.activity.setup.AccountSecurity; import com.android.email.activity.setup.AccountSettingsXL; +import com.android.email.mail.MessagingException; import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Mailbox; +import com.android.email.provider.EmailContent.Message; import android.app.ActionBar; import android.app.Activity; @@ -36,14 +40,12 @@ import android.content.Loader; import android.database.Cursor; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; -import android.view.animation.Animation; -import android.view.animation.Animation.AnimationListener; -import android.view.animation.AnimationUtils; import android.widget.TextView; import java.security.InvalidParameterException; @@ -64,9 +66,11 @@ public class MessageListXL extends Activity implements /* package */ static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds private Context mContext; + private Controller mController; private RefreshManager mRefreshManager; private final RefreshListener mMailRefreshManagerListener = new RefreshListener(); + private Controller.Result mControllerResult; private AccountSelectorAdapter mAccountsSelectorAdapter; private final ActionBarNavigationCallback mActionBarNavigationCallback @@ -82,12 +86,12 @@ public class MessageListXL extends Activity implements private RefreshTask mRefreshTask; + private BannerController mBannerController; private TextView mErrorMessageView; - /** True when {@link #mErrorMessageView} is fully shown, when it should be clickable. */ - private boolean mErrorMessageFullyShown; - - private Animation mErrorMessageShowAnimation; - private Animation mErrorMessageHideAnimation; + /** + * Id of the account that had a messaging exception most recently. + */ + private long mLastErrorAccountId; /** * Launch and open account's inbox. @@ -156,6 +160,7 @@ public class MessageListXL extends Activity implements mFragmentManager.onActivityViewReady(); mContext = getApplicationContext(); + mController = Controller.getInstance(this); mRefreshManager = RefreshManager.getInstance(this); mRefreshManager.registerListener(mMailRefreshManagerListener); @@ -177,12 +182,16 @@ public class MessageListXL extends Activity implements // so that it'll be easy to reuse for the phone activities. mErrorMessageView = (TextView) findViewById(R.id.error_message); mErrorMessageView.setOnClickListener(this); - - initAnimation(); + mBannerController = new BannerController(this, mErrorMessageView, + getResources().getDimensionPixelSize(R.dimen.error_message_height)); // Halt the progress indicator (we'll display it later when needed) setProgressBarIndeterminate(true); setProgressBarIndeterminateVisibility(false); + + mControllerResult = new ControllerResultUiThreadWrapper(new Handler(), + new ControllerResult()); + mController.addResultCallback(mControllerResult); } private void initFromIntent() { @@ -199,33 +208,6 @@ public class MessageListXL extends Activity implements } } - private void initAnimation() { - // Set up error message animations. - mErrorMessageShowAnimation = AnimationUtils.loadAnimation(this, R.anim.header_slide_in); - mErrorMessageShowAnimation.setAnimationListener(new AnimationListener() { - @Override public void onAnimationRepeat(Animation animation) { } - @Override public void onAnimationStart(Animation animation) { } - @Override - public void onAnimationEnd(Animation animation) { - mErrorMessageFullyShown = true; - } - }); - - mErrorMessageHideAnimation = AnimationUtils.loadAnimation(this, R.anim.header_slide_out); - mErrorMessageHideAnimation.setAnimationListener(new AnimationListener() { - @Override public void onAnimationRepeat(Animation animation) { } - @Override - public void onAnimationStart(Animation animation) { - mErrorMessageFullyShown = false; - } - - @Override - public void onAnimationEnd(Animation animation) { - mErrorMessageView.setVisibility(View.GONE); - } - }); - } - @Override protected void onSaveInstanceState(Bundle outState) { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { @@ -278,6 +260,7 @@ public class MessageListXL extends Activity implements @Override protected void onDestroy() { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Email.LOG_TAG, "MessageListXL onDestroy"); + mController.removeResultCallback(mControllerResult); Utility.cancelTaskInterrupt(mRefreshTask); mRefreshManager.unregisterListener(mMailRefreshManagerListener); mFragmentManager.onDestroy(); @@ -510,7 +493,6 @@ public class MessageListXL extends Activity implements @Override public void onLoadMessageError(String errorMessage) { - showErrorMessage(errorMessage); } @Override @@ -569,37 +551,31 @@ public class MessageListXL extends Activity implements updateProgressIcon(); } - private void showErrorMessage(String message) { - /* Note: All error messages come from the Controller.Result callback to the UI, - * but this class doesn't use it directly. Instead it uses the following callbacks. - * - * RefreshManager.Listener.onMessagingError for - * -updateMailboxListCallback - * -updateMailboxCallback - * -serviceCheckMailCallback - * -sendMailCallback - * - * MessageViewFragmentBase.Callback.onLoadMessageError for - * -loadMessageForViewCallback - * -loadAttachmentCallback - */ - - if (mErrorMessageView.getVisibility() == View.VISIBLE) { - // If an error is already shown, do nothing, not even changing the text, to avoid - // flicker. - return; + /** + * Call this when getting a connection error. + */ + private void showErrorMessage(String message, long accountId) { + if (mBannerController.show(message)) { + mLastErrorAccountId = accountId; } - mErrorMessageView.setText(message); - mErrorMessageView.setVisibility(View.VISIBLE); - mErrorMessageView.startAnimation(mErrorMessageShowAnimation); } - private void dismissErrorMessage() { - if (mErrorMessageFullyShown) { - mErrorMessageView.startAnimation(mErrorMessageHideAnimation); + /** + * Call this when the connection for an account is considered working. + */ + private void clearErrorMessage(long accountId) { + if (mLastErrorAccountId == accountId) { + dismissErrorMessage(); } } + /** + * Force dismiss the error banner. + */ + private void dismissErrorMessage() { + mBannerController.dismiss(); + } + /** * Load account list for the action bar. * @@ -672,7 +648,6 @@ public class MessageListXL extends Activity implements implements RefreshManager.Listener { @Override public void onMessagingError(final long accountId, long mailboxId, final String message) { - showErrorMessage(message); updateProgressIcon(); } @@ -842,4 +817,98 @@ public class MessageListXL extends Activity implements return true; } } + + /** + * A {@link Controller.Result} to detect connection status. + */ + private class ControllerResult extends Controller.Result { + @Override + public void sendMailCallback( + MessagingException result, long accountId, long messageId, int progress) { + handleError(result, accountId, progress); + } + + @Override + public void serviceCheckMailCallback( + MessagingException result, long accountId, long mailboxId, int progress, long tag) { + handleError(result, accountId, progress); + } + + @Override + public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, + int progress, int numNewMessages) { + handleError(result, accountId, progress); + } + + @Override + public void updateMailboxListCallback( + MessagingException result, long accountId, int progress) { + handleError(result, accountId, progress); + } + + @Override + public void loadAttachmentCallback( + MessagingException result, long messageId, long attachmentId, int progress) { + new AccountFinder(result, messageId, progress).execute(); + } + + @Override + public void loadMessageForViewCallback( + MessagingException result, long messageId, int progress) { + new AccountFinder(result, messageId, progress).execute(); + } + + /** + * AsyncTask to determine the account id from a message id. Used for + * {@link #loadAttachmentCallback} and {@link #loadMessageForViewCallback}, which don't + * report the underlying account ID. + */ + private class AccountFinder extends AsyncTask { + private final MessagingException mException; + private final long mMessageId; + private final int mProgress; + + public AccountFinder(MessagingException exception, long messageId, int progress) { + mException = exception; + mMessageId = messageId; + mProgress = progress; + } + + @Override + protected Long doInBackground(Void... params) { + if (mMessageId == -1) { + return null; // Message ID unknown + } + Message m = Message.restoreMessageWithId(MessageListXL.this, mMessageId); + return m != null ? m.mAccountKey : null; + } + + @Override + protected void onPostExecute(Long accountId) { + if ((accountId == null) || isCancelled()) { + return; + } + handleError(mException, accountId, mProgress); + } + } + + private void handleError(MessagingException result, long accountId, int progress) { + if (!isRegistered()) { + // This ControllerResult may be already unregistered, because of the asynctask. + return; + } + if (accountId == -1) { + return; + } + if (result == null) { + if (progress > 0) { + // Connection now working. + clearErrorMessage(accountId); + } + } else { + // Connection error. + showErrorMessage(result.getUiErrorMessage(MessageListXL.this), accountId); + } + } + } }