Get rid of Handlers and make activities (more) BG thread free.

Part 1: MessageView

- It's an attempt to get rid of Handlers from Activities, and
  reduce the amount of code that runs run a BG thread in them.

- Introduced ResultUiThreadWrapper, which wraps another Controller.Result
  and make callbacks get called on the UI thread.

  - It'll make the logic in ControllerResults cleaner and more straightforward.

  - ResultUiThreadWrapper isn't too memory efficient because it allocates a
    Runnable even if the wrappee's target method is empty.
    However these callbacks don't get called often, and optimizing it would
    make code more complicated, so I don't think it's worth optimizing.

- Now we can assume all the methods in activities except
  AsyncTask.doInBackground runs on the UI thread, with some special exceptions
  like MediaScannerNotifier.
  In my previous abandoned change, I named methods that can run on BG threads
  '*OnUiThread', but now there's no need to do that.

  This also means we can minimize the use of synchronizations.

Change-Id: Ia6d9d2a266ebf5a4b23d712e9eaea3272adbd2a6
This commit is contained in:
Makoto Onuki 2010-05-24 11:35:43 -07:00
parent 7e5ba0e1ea
commit 7e24c6c6f9
3 changed files with 181 additions and 171 deletions

View File

@ -0,0 +1,91 @@
/*
* 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;
import com.android.email.Controller.Result;
import com.android.email.mail.MessagingException;
import android.app.Activity;
/**
* A {@link Result} that wraps another {@link Result} and makes sure methods gets called back
* on the UI thread.
*/
public class ControllerResultUiThreadWrapper implements Result {
private final Activity mActivity;
private final Result mWrappee;
public ControllerResultUiThreadWrapper(Activity activity, Result wrappee) {
mActivity = activity;
mWrappee = wrappee;
}
public void loadAttachmentCallback(final MessagingException result, final long messageId,
final long attachmentId, final int progress) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.loadAttachmentCallback(result, messageId, attachmentId, progress);
}
});
}
public void loadMessageForViewCallback(final MessagingException result,
final long messageId, final int progress) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.loadMessageForViewCallback(result, messageId, progress);
}
});
}
public void sendMailCallback(final MessagingException result, final long accountId,
final long messageId, final int progress) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.sendMailCallback(result, accountId, messageId, progress);
}
});
}
public void serviceCheckMailCallback(final MessagingException result, final long accountId,
final long mailboxId, final int progress, final long tag) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.serviceCheckMailCallback(result, accountId, mailboxId, progress, tag);
}
});
}
public void updateMailboxCallback(final MessagingException result, final long accountId,
final long mailboxId, final int progress, final int numNewMessages) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.updateMailboxCallback(result, accountId, mailboxId, progress,
numNewMessages);
}
});
}
public void updateMailboxListCallback(final MessagingException result, final long accountId,
final int progress) {
mActivity.runOnUiThread(new Runnable() {
public void run() {
mWrappee.updateMailboxListCallback(result, accountId, progress);
}
});
}
}

View File

@ -26,6 +26,7 @@ import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.TypedArray;
@ -39,6 +40,7 @@ import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -615,4 +617,19 @@ public class Utility {
public static ByteArrayInputStream streamFromAsciiString(String ascii) {
return new ByteArrayInputStream(toAscii(ascii));
}
/**
* A thread safe way to show a Toast. This method uses {@link Activity#runOnUiThread}, so it
* can be called on any thread.
*
* @param activity Parent activity.
* @param resId Resource ID of the message string.
*/
public static void showToast(final Activity activity, final int resId) {
activity.runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(activity, resId, Toast.LENGTH_LONG).show();
}
});
}
}

View File

@ -19,6 +19,7 @@ package com.android.email.activity;
import com.android.email.Controller;
import com.android.email.Email;
import com.android.email.R;
import com.android.email.ControllerResultUiThreadWrapper;
import com.android.email.Utility;
import com.android.email.mail.Address;
import com.android.email.mail.MeetingInfo;
@ -156,9 +157,8 @@ public class MessageView extends Activity implements OnClickListener {
private Drawable mFavoriteIconOn;
private Drawable mFavoriteIconOff;
private MessageViewHandler mHandler;
private Controller mController;
private ControllerResults mControllerCallback;
private Controller.Result mControllerCallback;
private View mMoveToNewer;
private View mMoveToOlder;
@ -176,136 +176,7 @@ public class MessageView extends Activity implements OnClickListener {
// this is true when reply & forward are disabled, such as messages in the trash
private boolean mDisableReplyAndForward;
private class MessageViewHandler extends Handler {
private static final int MSG_PROGRESS = 1;
private static final int MSG_ATTACHMENT_PROGRESS = 2;
private static final int MSG_LOAD_CONTENT_URI = 3;
private static final int MSG_SET_ATTACHMENTS_ENABLED = 4;
private static final int MSG_LOAD_BODY_ERROR = 5;
private static final int MSG_NETWORK_ERROR = 6;
private static final int MSG_FETCHING_ATTACHMENT = 10;
private static final int MSG_VIEW_ATTACHMENT_ERROR = 12;
private static final int MSG_UPDATE_ATTACHMENT_ICON = 18;
private static final int MSG_FINISH_LOAD_ATTACHMENT = 19;
@Override
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case MSG_PROGRESS:
setProgressBarIndeterminateVisibility(msg.arg1 != 0);
break;
case MSG_ATTACHMENT_PROGRESS:
boolean progress = (msg.arg1 != 0);
if (progress) {
mProgressDialog.setMessage(
getString(R.string.message_view_fetching_attachment_progress,
mLoadAttachmentName));
mProgressDialog.show();
} else {
mProgressDialog.dismiss();
}
setProgressBarIndeterminateVisibility(progress);
break;
case MSG_LOAD_CONTENT_URI:
String uriString = (String) msg.obj;
if (mMessageContentView != null) {
mMessageContentView.loadUrl(uriString);
}
break;
case MSG_SET_ATTACHMENTS_ENABLED:
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
AttachmentInfo attachment =
(AttachmentInfo) mAttachments.getChildAt(i).getTag();
attachment.viewButton.setEnabled(msg.arg1 == 1);
attachment.downloadButton.setEnabled(msg.arg1 == 1);
}
break;
case MSG_LOAD_BODY_ERROR:
Toast.makeText(MessageView.this,
R.string.error_loading_message_body, Toast.LENGTH_LONG).show();
break;
case MSG_NETWORK_ERROR:
Toast.makeText(MessageView.this,
R.string.status_network_error, Toast.LENGTH_LONG).show();
break;
case MSG_FETCHING_ATTACHMENT:
Toast.makeText(MessageView.this,
getString(R.string.message_view_fetching_attachment_toast),
Toast.LENGTH_SHORT).show();
break;
case MSG_VIEW_ATTACHMENT_ERROR:
Toast.makeText(MessageView.this,
getString(R.string.message_view_display_attachment_toast),
Toast.LENGTH_SHORT).show();
break;
case MSG_UPDATE_ATTACHMENT_ICON:
((AttachmentInfo) mAttachments.getChildAt(msg.arg1).getTag())
.iconView.setImageBitmap((Bitmap) msg.obj);
break;
case MSG_FINISH_LOAD_ATTACHMENT:
long attachmentId = (Long)msg.obj;
doFinishLoadAttachment(attachmentId);
break;
default:
super.handleMessage(msg);
}
}
public void attachmentProgress(boolean progress) {
android.os.Message msg = android.os.Message.obtain(this, MSG_ATTACHMENT_PROGRESS);
msg.arg1 = progress ? 1 : 0;
sendMessage(msg);
}
public void progress(boolean progress) {
android.os.Message msg = android.os.Message.obtain(this, MSG_PROGRESS);
msg.arg1 = progress ? 1 : 0;
sendMessage(msg);
}
public void loadContentUri(String uriString) {
android.os.Message msg = android.os.Message.obtain(this, MSG_LOAD_CONTENT_URI);
msg.obj = uriString;
sendMessage(msg);
}
public void setAttachmentsEnabled(boolean enabled) {
android.os.Message msg = android.os.Message.obtain(this, MSG_SET_ATTACHMENTS_ENABLED);
msg.arg1 = enabled ? 1 : 0;
sendMessage(msg);
}
public void loadBodyError() {
sendEmptyMessage(MSG_LOAD_BODY_ERROR);
}
public void networkError() {
sendEmptyMessage(MSG_NETWORK_ERROR);
}
public void fetchingAttachment() {
sendEmptyMessage(MSG_FETCHING_ATTACHMENT);
}
public void attachmentViewError() {
sendEmptyMessage(MSG_VIEW_ATTACHMENT_ERROR);
}
public void updateAttachmentIcon(int pos, Bitmap icon) {
android.os.Message msg = android.os.Message.obtain(this, MSG_UPDATE_ATTACHMENT_ICON);
msg.arg1 = pos;
msg.obj = icon;
sendMessage(msg);
}
public void finishLoadAttachment(long attachmentId) {
android.os.Message msg = android.os.Message.obtain(this, MSG_FINISH_LOAD_ATTACHMENT);
msg.obj = Long.valueOf(attachmentId);
sendMessage(msg);
}
}
/**
/**
* Encapsulates known information about a single attachment.
*/
private static class AttachmentInfo {
@ -346,8 +217,7 @@ public class MessageView extends Activity implements OnClickListener {
super.onCreate(icicle);
setContentView(R.layout.message_view);
mHandler = new MessageViewHandler();
mControllerCallback = new ControllerResults();
mControllerCallback = new ControllerResultUiThreadWrapper(this, new ControllerResults());
mSubjectView = (TextView) findViewById(R.id.subject);
mFromView = (TextView) findViewById(R.id.from);
@ -408,8 +278,14 @@ public class MessageView extends Activity implements OnClickListener {
mController = Controller.getInstance(getApplication());
// This observer is used to watch for external changes to the message list
mCursorObserver = new ContentObserver(mHandler){
// Set up ContentObserver.
// This observer is used to watch for external changes to the message list.
// Pass a Handler so that onChange() gets called back in the UI thread.
// (All we want to do here is to run an AsyncTask, so it could run on a bg thread, but doing
// so would require synchronization to protect mLoadMessageListTask. Let's just do it on
// the UI thread to keep it simple.)
mCursorObserver = new ContentObserver(new Handler()){
@Override
public void onChange(boolean selfChange) {
// get a new message list cursor, but only if we already had one
@ -504,11 +380,8 @@ public class MessageView extends Activity implements OnClickListener {
public void onDestroy() {
super.onDestroy();
cancelAllTasks();
// This is synchronized because the listener accesses mMessageContentView from its thread
synchronized (this) {
mMessageContentView.destroy();
mMessageContentView = null;
}
mMessageContentView.destroy();
mMessageContentView = null;
// the cursor was closed in onPause()
}
@ -988,7 +861,7 @@ public class MessageView extends Activity implements OnClickListener {
if (attachment.attachmentId == attachmentId) {
Bitmap previewIcon = getPreviewIcon(attachment);
if (previewIcon != null) {
mHandler.updateAttachmentIcon(i, previewIcon);
attachment.iconView.setImageBitmap(previewIcon);
}
return;
}
@ -1231,6 +1104,7 @@ public class MessageView extends Activity implements OnClickListener {
private class LoadBodyTask extends AsyncTask<Void, Void, String[]> {
private long mId;
private boolean mErrorLoadingMessageBody;
/**
* Special constructor to cache some local info
@ -1252,7 +1126,7 @@ public class MessageView extends Activity implements OnClickListener {
// This catches SQLiteException as well as other RTE's we've seen from the
// database calls, such as IllegalStateException
Log.d(Email.LOG_TAG, "Exception while loading message body: " + re.toString());
mHandler.loadBodyError();
mErrorLoadingMessageBody = true;
return new String[] { null, null };
}
}
@ -1260,6 +1134,9 @@ public class MessageView extends Activity implements OnClickListener {
@Override
protected void onPostExecute(String[] results) {
if (results == null) {
if (mErrorLoadingMessageBody) {
Utility.showToast(MessageView.this, R.string.error_loading_message_body);
}
return;
}
reloadUiFromBody(results[0], results[1]); // text, html
@ -1434,7 +1311,8 @@ public class MessageView extends Activity implements OnClickListener {
}
/**
* Controller results listener. This completely replaces MessagingListener
* Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper},
* so all methods are called on the UI thread.
*/
private class ControllerResults implements Controller.Result {
@ -1448,12 +1326,12 @@ public class MessageView extends Activity implements OnClickListener {
if (result == null) {
switch (progress) {
case 0:
mHandler.progress(true);
mHandler.loadContentUri("file:///android_asset/loading.html");
setProgressBarIndeterminateVisibility(true);
loadBodyContent("file:///android_asset/loading.html");
break;
case 100:
mWaitForLoadMessageId = -1;
mHandler.progress(false);
setProgressBarIndeterminateVisibility(false);
// reload UI and reload everything else too
// pass false to LoadMessageTask to prevent looping here
cancelAllTasks();
@ -1466,9 +1344,15 @@ public class MessageView extends Activity implements OnClickListener {
}
} else {
mWaitForLoadMessageId = -1;
mHandler.progress(false);
mHandler.networkError();
mHandler.loadContentUri("file:///android_asset/empty.html");
setProgressBarIndeterminateVisibility(false);
Utility.showToast(MessageView.this, R.string.status_network_error);
loadBodyContent("file:///android_asset/empty.html");
}
}
private void loadBodyContent(String uri) {
if (mMessageContentView != null) {
mMessageContentView.loadUrl(uri);
}
}
@ -1478,28 +1362,49 @@ public class MessageView extends Activity implements OnClickListener {
if (result == null) {
switch (progress) {
case 0:
mHandler.setAttachmentsEnabled(false);
mHandler.attachmentProgress(true);
mHandler.fetchingAttachment();
enableAttachments(false);
showFetchingAttachmentProgress(true);
Utility.showToast(MessageView.this,
R.string.message_view_fetching_attachment_toast);
break;
case 100:
mHandler.setAttachmentsEnabled(true);
mHandler.attachmentProgress(false);
enableAttachments(true);
showFetchingAttachmentProgress(false);
updateAttachmentThumbnail(attachmentId);
mHandler.finishLoadAttachment(attachmentId);
doFinishLoadAttachment(attachmentId);
break;
default:
// do nothing - we don't have a progress bar at this time
break;
}
} else {
mHandler.setAttachmentsEnabled(true);
mHandler.attachmentProgress(false);
mHandler.networkError();
enableAttachments(true);
showFetchingAttachmentProgress(false);
Utility.showToast(MessageView.this, R.string.status_network_error);
}
}
}
private void enableAttachments(boolean enable) {
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
AttachmentInfo attachment = (AttachmentInfo) mAttachments.getChildAt(i).getTag();
attachment.viewButton.setEnabled(enable);
attachment.downloadButton.setEnabled(enable);
}
}
private void showFetchingAttachmentProgress(boolean show) {
if (show) {
mProgressDialog.setMessage(
getString(R.string.message_view_fetching_attachment_progress,
mLoadAttachmentName));
mProgressDialog.show();
} else {
mProgressDialog.dismiss();
}
setProgressBarIndeterminateVisibility(show);
}
public void updateMailboxCallback(MessagingException result, long accountId,
long mailboxId, int progress, int numNewMessages) {
if (result != null || progress == 100) {
@ -1635,7 +1540,7 @@ public class MessageView extends Activity implements OnClickListener {
getString(R.string.message_view_status_attachment_saved), file.getName()),
Toast.LENGTH_LONG).show();
new MediaScannerNotifier(this, file, mHandler);
new MediaScannerNotifier(this, file);
} catch (IOException ioe) {
Toast.makeText(MessageView.this,
getString(R.string.message_view_status_attachment_not_saved),
@ -1649,7 +1554,7 @@ public class MessageView extends Activity implements OnClickListener {
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
} catch (ActivityNotFoundException e) {
mHandler.attachmentViewError();
Utility.showToast(this, R.string.message_view_display_attachment_toast);
// TODO: Add a proper warning message (and lots of upstream cleanup to prevent
// it from happening) in the next release.
}
@ -1662,16 +1567,14 @@ public class MessageView extends Activity implements OnClickListener {
* to start an ACTION_VIEW activity for the attachment.
*/
private static class MediaScannerNotifier implements MediaScannerConnectionClient {
private Context mContext;
private Activity mActivity;
private MediaScannerConnection mConnection;
private File mFile;
private MessageViewHandler mHandler;
public MediaScannerNotifier(Context context, File file, MessageViewHandler handler) {
mContext = context;
public MediaScannerNotifier(Activity activity, File file) {
mActivity = activity;
mFile = file;
mHandler = handler;
mConnection = new MediaScannerConnection(context, this);
mConnection = new MediaScannerConnection(mActivity.getApplicationContext(), this);
mConnection.connect();
}
@ -1684,16 +1587,15 @@ public class MessageView extends Activity implements OnClickListener {
if (uri != null) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
mContext.startActivity(intent);
mActivity.startActivity(intent);
}
} catch (ActivityNotFoundException e) {
mHandler.attachmentViewError();
Utility.showToast(mActivity, R.string.message_view_display_attachment_toast);
// TODO: Add a proper warning message (and lots of upstream cleanup to prevent
// it from happening) in the next release.
} finally {
mConnection.disconnect();
mContext = null;
mHandler = null;
mActivity = null;
}
}
}