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
This commit is contained in:
Makoto Onuki 2010-12-07 15:17:52 -08:00
parent 4d02297f4a
commit 45e04b009d
7 changed files with 256 additions and 119 deletions

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:fromYDelta="-100%"
android:toYDelta="0"
android:duration="300" />
</set>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:fromYDelta="0"
android:toYDelta="-100%"
android:duration="100" />
</set>

View File

@ -14,19 +14,13 @@
limitations under the License.
-->
<FrameLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<include
android:id="@+id/three_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/three_pane"
/>
<!-- Error message goes over the normal view -->
<!-- STOPSHIP not pixel perfect -->
<TextView
@ -41,6 +35,12 @@
android:singleLine="true"
android:ellipsize="end"
android:background="#ffff66"
android:visibility="gone"
/>
</FrameLayout>
<include
android:id="@+id/three_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/three_pane"
/>
</LinearLayout>

View File

@ -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;
}

View File

@ -41,6 +41,12 @@ public class ControllerResultUiThreadWrapper<T extends Result> 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();

View File

@ -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();
}
}

View File

@ -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<ControllerResult>(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<Void, Void, Long> {
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);
}
}
}
}