From 8112732376d4cc033ee515a6531852ef42266929 Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Wed, 27 Apr 2011 17:55:13 -0700 Subject: [PATCH] Move more UI stuff from activity to the UI controller - Now all the UI stuff is owned by the UI controller - Except temporary UI (exchange search and per-mailbox-settings) - Except error banner This should be moved too eventually, but I consider it as a low-priority. I'll leave it as-is for the time being. - Moved RefreshTask too. The spec for refresh has dependency to the UI. (i.e. implicit refresh of the mailbox list may not be necessary for the phone.) Also renamed the main activity to EmailActivity. Change-Id: I00585856bdacf69aa4e104178a5cf7352ff6d592 --- AndroidManifest.xml | 2 +- .../activity/AccountSelectorAdapter.java | 9 +- .../email/activity/ActivityHelper.java | 2 +- ...{MessageListXL.java => EmailActivity.java} | 329 ++-------------- .../email/activity/UIControllerTwoPane.java | 360 ++++++++++++++++-- src/com/android/email/activity/Welcome.java | 6 +- ...> UIControllerTwoPaneRefreshTaskTest.java} | 16 +- 7 files changed, 375 insertions(+), 349 deletions(-) rename src/com/android/email/activity/{MessageListXL.java => EmailActivity.java} (64%) rename tests/src/com/android/email/activity/{MessageListXLRefreshTaskTest.java => UIControllerTwoPaneRefreshTaskTest.java} (91%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 835f28009..29395c948 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -210,7 +210,7 @@ > + android:name=".activity.EmailActivity"> diff --git a/src/com/android/email/activity/AccountSelectorAdapter.java b/src/com/android/email/activity/AccountSelectorAdapter.java index abf99f4e2..d09c6ae8b 100644 --- a/src/com/android/email/activity/AccountSelectorAdapter.java +++ b/src/com/android/email/activity/AccountSelectorAdapter.java @@ -36,12 +36,9 @@ import android.widget.CursorAdapter; import android.widget.TextView; /** - * Adapter for the account selector on {@link MessageListXL}. + * Adapter for the account selector on {@link UIControllerTwoPane}. * * TODO Test it! - * TODO Use layout? Or use the standard resources that ActionBarDemo uses? - * TODO Revisit the sort order when we get more detailed UI spec. (current sort order makes things - * simpler for now.) Maybe we can just use SimpleCursorAdapter. */ public class AccountSelectorAdapter extends CursorAdapter { /** Projection used to query from Account */ @@ -78,8 +75,8 @@ public class AccountSelectorAdapter extends CursorAdapter { return new AccountsLoader(context); } - public AccountSelectorAdapter(Context context, Cursor c) { - super(context, c, 0 /* no auto-requery */); + public AccountSelectorAdapter(Context context) { + super(context, null, 0 /* no auto-requery */); mContext = context; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } diff --git a/src/com/android/email/activity/ActivityHelper.java b/src/com/android/email/activity/ActivityHelper.java index 57297f843..bb6521798 100644 --- a/src/com/android/email/activity/ActivityHelper.java +++ b/src/com/android/email/activity/ActivityHelper.java @@ -35,7 +35,7 @@ import android.view.WindowManager; /** * Various methods that are used by both 1-pane and 2-pane activities. * - *

Common code used by {@link MessageListXL}, {@link MessageList} and other activities go here. + *

Common code used by {@link EmailActivity}, {@link MessageList} and other activities go here. * Probably there's a nicer way to do this, if we re-design these classes more throughly. * However, without knowing what the phone UI will be, all such work can easily end up being * over-designed or totally useless. For now this pattern will do... diff --git a/src/com/android/email/activity/MessageListXL.java b/src/com/android/email/activity/EmailActivity.java similarity index 64% rename from src/com/android/email/activity/MessageListXL.java rename to src/com/android/email/activity/EmailActivity.java index b4054e098..41c1454f2 100644 --- a/src/com/android/email/activity/MessageListXL.java +++ b/src/com/android/email/activity/EmailActivity.java @@ -16,14 +16,12 @@ 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.MessagingExceptionStrings; import com.android.email.R; import com.android.email.RefreshManager; -import com.android.email.activity.setup.AccountSettingsXL; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent.Account; @@ -33,12 +31,10 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.Utility; -import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.Fragment; -import android.app.LoaderManager.LoaderCallbacks; import android.app.SearchManager; import android.content.ContentResolver; import android.content.ContentUris; @@ -46,8 +42,6 @@ import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.Loader; -import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; @@ -64,31 +58,22 @@ import java.security.InvalidParameterException; * * Because this activity is device agnostic, so most of the UI aren't owned by this, but by * the UIController. - * - * TODO: Account spinner should also be moved out of this class. (to the UIController or to a - * sepate class.) */ -public class MessageListXL extends Activity implements View.OnClickListener { +public class EmailActivity extends Activity implements View.OnClickListener { private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; private static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; private static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; - private static final int LOADER_ID_ACCOUNT_LIST = 0; - /* package */ static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds - /* package */ static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds + + /** Loader IDs starting with this is safe to use from UIControllers. */ + static final int UI_CONTROLLER_LOADER_ID_BASE = 100; private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1; private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2; private Context mContext; private Controller mController; - private RefreshManager mRefreshManager; - private final RefreshListener mRefreshListener = new RefreshListener(); private Controller.Result mControllerResult; - private AccountSelectorAdapter mAccountsSelectorAdapter; - private final ActionBarNavigationCallback mActionBarNavigationCallback = - new ActionBarNavigationCallback(); - private final UIControllerTwoPane mUIController = new UIControllerTwoPane(this); private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); @@ -107,7 +92,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { * @param accountId If -1, default account will be used. */ public static void actionOpenAccount(Activity fromActivity, long accountId) { - Intent i = IntentUtilities.createRestartAppIntent(fromActivity, MessageListXL.class); + Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); if (accountId != -1) { i.putExtra(EXTRA_ACCOUNT_ID, accountId); } @@ -125,7 +110,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { if (accountId == -1 || mailboxId == -1) { throw new InvalidParameterException(); } - Intent i = IntentUtilities.createRestartAppIntent(fromActivity, MessageListXL.class); + Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); i.putExtra(EXTRA_ACCOUNT_ID, accountId); i.putExtra(EXTRA_MAILBOX_ID, mailboxId); fromActivity.startActivity(i); @@ -144,7 +129,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { if (accountId == -1 || mailboxId == -1 || messageId == -1) { throw new InvalidParameterException(); } - Intent i = IntentUtilities.createRestartAppIntent(fromActivity, MessageListXL.class); + Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); i.putExtra(EXTRA_ACCOUNT_ID, accountId); i.putExtra(EXTRA_MAILBOX_ID, mailboxId); i.putExtra(EXTRA_MESSAGE_ID, messageId); @@ -153,7 +138,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onCreate"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onCreate"); super.onCreate(savedInstanceState); ActivityHelper.debugSetWindowFlags(this); setContentView(R.layout.message_list_xl); @@ -165,12 +150,6 @@ public class MessageListXL extends Activity implements View.OnClickListener { mControllerResult = new ControllerResultUiThreadWrapper(new Handler(), new ControllerResult()); mController.addResultCallback(mControllerResult); - mRefreshManager = RefreshManager.getInstance(this); - mRefreshManager.registerListener(mRefreshListener); - - mAccountsSelectorAdapter = new AccountSelectorAdapter(this, null); - - loadAccounts(); // Set up views // TODO Probably better to extract mErrorMessageView related code into a separate class, @@ -190,6 +169,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { // See UIControllerTwoPane.preFragmentTransactionCheck() initFromIntent(); } + mUIController.onActivityCreated(); } private void initFromIntent() { @@ -209,7 +189,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected void onSaveInstanceState(Bundle outState) { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { - Log.d(Logging.LOG_TAG, "MessageListXL onSaveInstanceState"); + Log.d(Logging.LOG_TAG, "" + this + " onSaveInstanceState"); } super.onSaveInstanceState(outState); mUIController.onSaveInstanceState(outState); @@ -218,7 +198,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override public void onAttachFragment(Fragment fragment) { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { - Log.d(Logging.LOG_TAG, "MessageListXL onAttachFragment fragment=" + fragment); + Log.d(Logging.LOG_TAG, "" + this + " onAttachFragment fragment=" + fragment); } super.onAttachFragment(fragment); mUIController.onAttachFragment(fragment); @@ -226,7 +206,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected void onStart() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onStart"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onStart"); super.onStart(); mUIController.onStart(); @@ -261,7 +241,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { msg.mTimeStamp = Long.MAX_VALUE; // Sort on top msg.save(mContext); - actionOpenMessage(MessageListXL.this, accountId, searchMailbox.mId, msg.mId); + actionOpenMessage(EmailActivity.this, accountId, searchMailbox.mId, msg.mId); Utility.runAsync(new Runnable() { @Override public void run() { @@ -274,7 +254,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected void onResume() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onResume"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onResume"); super.onResume(); mUIController.onResume(); /** @@ -287,24 +267,23 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected void onPause() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onPause"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onPause"); super.onPause(); mUIController.onPause(); } @Override protected void onStop() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onStop"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onStop"); super.onStop(); mUIController.onStop(); } @Override protected void onDestroy() { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "MessageListXL onDestroy"); + if (Email.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, "" + this + " onDestroy"); mController.removeResultCallback(mControllerResult); mTaskTracker.cancellAllInterrupt(); - mRefreshManager.unregisterListener(mRefreshListener); mUIController.onDestroy(); super.onDestroy(); } @@ -312,27 +291,12 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override public void onBackPressed() { if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { - Log.d(Logging.LOG_TAG, "MessageListXL onBackPressed"); + Log.d(Logging.LOG_TAG, "" + this + " onBackPressed"); } - onBackPressed(true); - } - - /** - * Performs the back action. - * - * @param isSystemBackKey true if the system back key was pressed. Otherwise, - * false [e.g. the home icon on action bar were pressed]. - */ - private boolean onBackPressed(boolean isSystemBackKey) { - if (mUIController.onBackPressed(isSystemBackKey)) { - return true; - } - if (isSystemBackKey) { - // Perform the default behavior. + if (!mUIController.onBackPressed(true)) { + // Not handled by UIController -- perform the default. i.e. close the app. super.onBackPressed(); - return true; } - return false; } @Override @@ -344,14 +308,6 @@ public class MessageListXL extends Activity implements View.OnClickListener { } } - /** - * Called by the UIController when the current account has changed. - */ - public void onAccountChanged(long accountId) { - updateRefreshProgress(); - loadAccounts(); // This will update the account spinner, and select the account. - } - /** * Force dismiss the error banner. */ @@ -359,96 +315,6 @@ public class MessageListXL extends Activity implements View.OnClickListener { mErrorBanner.dismiss(); } - /** - * Load account list for the action bar. - * - * If there's only one account configured, show the account name in the action bar. - * If more than one account are configured, show a spinner in the action bar, and select the - * current account. - */ - private void loadAccounts() { - getLoaderManager().initLoader(LOADER_ID_ACCOUNT_LIST, null, new LoaderCallbacks() { - @Override - public Loader onCreateLoader(int id, Bundle args) { - return AccountSelectorAdapter.createLoader(mContext); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - updateAccountList(data); - } - - @Override - public void onLoaderReset(Loader loader) { - mAccountsSelectorAdapter.swapCursor(null); - } - }); - } - - private void updateAccountList(Cursor accountsCursor) { - final int count = accountsCursor.getCount(); - if (count == 0) { - // Open Welcome, which in turn shows the adding a new account screen. - Welcome.actionStart(this); - finish(); - return; - } - - // If ony one acount, don't show dropdown. - final ActionBar ab = getActionBar(); - if (count == 1) { - accountsCursor.moveToFirst(); - - // Show the account name as the title. - ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE); - ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); - ab.setTitle(AccountSelectorAdapter.getAccountDisplayName(accountsCursor)); - return; - } - - // Find the currently selected account, and select it. - int defaultSelection = 0; - if (mUIController.isAccountSelected()) { - accountsCursor.moveToPosition(-1); - int i = 0; - while (accountsCursor.moveToNext()) { - final long accountId = AccountSelectorAdapter.getAccountId(accountsCursor); - if (accountId == mUIController.getUIAccountId()) { - defaultSelection = i; - break; - } - i++; - } - } - - // Update the dropdown list. - mAccountsSelectorAdapter.swapCursor(accountsCursor); - - // Don't show the title. - ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); - ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); - ab.setListNavigationCallbacks(mAccountsSelectorAdapter, mActionBarNavigationCallback); - ab.setSelectedNavigationItem(defaultSelection); - } - - private void selectAccount(long accountId) { - if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Account selected: accountId=" + accountId); - } - // TODO UIManager should do the check eventually, but it's necessary for now. - if (accountId != mUIController.getUIAccountId()) { - mUIController.openAccount(accountId); - } - } - - private class ActionBarNavigationCallback implements ActionBar.OnNavigationListener { - @Override - public boolean onNavigationItemSelected(int itemPosition, long accountId) { - selectAccount(accountId); - return true; - } - } - private class RefreshListener implements RefreshManager.Listener { @Override @@ -474,9 +340,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.message_list_xl_option, menu); - return true; + return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); } @Override @@ -496,10 +360,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { menu.findItem(R.id.sync_lookback).setVisible(isEas); menu.findItem(R.id.sync_frequency).setVisible(isEas); - ActivityHelper.updateRefreshMenuIcon(menu.findItem(R.id.refresh), - mUIController.isRefreshEnabled(), - mUIController.isRefreshInProgress()); - return super.onPrepareOptionsMenu(menu); + return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); } @Override @@ -520,7 +381,7 @@ public class MessageListXL extends Activity implements View.OnClickListener { getContentResolver().update( ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), cv, null, null); - onRefresh(); + mUIController.onRefresh(); } } // STOPSHIP Temporary mailbox settings UI. If this ends up being useful, it should @@ -602,16 +463,10 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override public boolean onOptionsItemSelected(MenuItem item) { + if (mUIController.onOptionsItemSelected(item)) { + return true; + } switch (item.getItemId()) { - case android.R.id.home: - // Comes from the action bar when the app icon on the left is pressed. - // It works like a back press, but it won't close the activity. - return onBackPressed(false); - case R.id.compose: - return onCompose(); - case R.id.refresh: - onRefresh(); - return true; // STOPSHIP Temporary mailbox settings UI case R.id.sync_lookback: showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG); @@ -623,138 +478,10 @@ public class MessageListXL extends Activity implements View.OnClickListener { case R.id.search: onSearchRequested(); return true; - case R.id.account_settings: - return onAccountSettings(); } return super.onOptionsItemSelected(item); } - private boolean onCompose() { - if (!mUIController.isAccountSelected()) { - return false; // this shouldn't really happen - } - MessageCompose.actionCompose(this, mUIController.getActualAccountId()); - return true; - } - - private boolean onAccountSettings() { - AccountSettingsXL.actionSettings(this, mUIController.getActualAccountId()); - return true; - } - - private void onRefresh() { - // Cancel previously running instance if any. - new RefreshTask(mTaskTracker, this, mUIController.getActualAccountId(), - mUIController.getMailboxId()).cancelPreviousAndExecuteParallel(); - } - - /** - * TODO Move this to UIController, as requirement for this may change for the phone. - * (e.g. should we still do the implicit refresh of mailbox list on the phone?) - * - * Class to handle refresh. - * - * When the user press "refresh", - *

- */ - /* package */ static class RefreshTask extends EmailAsyncTask { - private final Clock mClock; - private final Context mContext; - private final long mAccountId; - private final long mMailboxId; - private final RefreshManager mRefreshManager; - /* package */ long mInboxId; - - public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, - long mailboxId) { - this(tracker, context, accountId, mailboxId, Clock.INSTANCE, - RefreshManager.getInstance(context)); - } - - /* package */ RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, - long mailboxId, Clock clock, RefreshManager refreshManager) { - super(tracker); - mClock = clock; - mContext = context; - mRefreshManager = refreshManager; - mAccountId = accountId; - mMailboxId = mailboxId; - } - - /** - * Do DB access on a worker thread. - */ - @Override - protected Boolean doInBackground(Void... params) { - mInboxId = Account.getInboxId(mContext, mAccountId); - return Mailbox.isRefreshable(mContext, mMailboxId); - } - - /** - * Do the actual refresh. - */ - @Override - protected void onPostExecute(Boolean isCurrentMailboxRefreshable) { - if (isCancelled() || isCurrentMailboxRefreshable == null) { - return; - } - if (isCurrentMailboxRefreshable) { - mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false); - } - // Refresh mailbox list - if (mAccountId != -1) { - if (shouldRefreshMailboxList()) { - mRefreshManager.refreshMailboxList(mAccountId); - } - } - // Refresh inbox - if (shouldAutoRefreshInbox()) { - mRefreshManager.refreshMessageList(mAccountId, mInboxId, false); - } - } - - /** - * @return true if the mailbox list of the current account hasn't been refreshed - * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}. - */ - /* package */ boolean shouldRefreshMailboxList() { - if (mRefreshManager.isMailboxListRefreshing(mAccountId)) { - return false; - } - final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId) - + MAILBOX_REFRESH_MIN_INTERVAL; - if (nextRefreshTime > mClock.getTime()) { - return false; - } - return true; - } - - /** - * @return true if the inbox of the current account hasn't been refreshed - * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. - */ - /* package */ boolean shouldAutoRefreshInbox() { - if (mInboxId == mMailboxId) { - return false; // Current ID == inbox. No need to auto-refresh. - } - if (mRefreshManager.isMessageListRefreshing(mInboxId)) { - return false; - } - final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId) - + INBOX_AUTO_REFRESH_MIN_INTERVAL; - if (nextRefreshTime > mClock.getTime()) { - return false; - } - return true; - } - } /** * A {@link Controller.Result} to detect connection status. @@ -814,14 +541,14 @@ public class MessageListXL extends Activity implements View.OnClickListener { @Override protected String doInBackground(Void... params) { Account account = - Account.restoreAccountWithId(MessageListXL.this, accountId); + Account.restoreAccountWithId(EmailActivity.this, accountId); return (account == null) ? null : account.mDisplayName; } @Override protected void onPostExecute(String accountName) { String message = - MessagingExceptionStrings.getErrorString(MessageListXL.this, result); + MessagingExceptionStrings.getErrorString(EmailActivity.this, result); if (!TextUtils.isEmpty(accountName)) { // TODO Use properly designed layout. Don't just concatenate strings; // which is generally poor for I18N. diff --git a/src/com/android/email/activity/UIControllerTwoPane.java b/src/com/android/email/activity/UIControllerTwoPane.java index 651152cd9..b89873b0d 100644 --- a/src/com/android/email/activity/UIControllerTwoPane.java +++ b/src/com/android/email/activity/UIControllerTwoPane.java @@ -16,22 +16,32 @@ package com.android.email.activity; +import com.android.email.Clock; import com.android.email.Email; import com.android.email.Preferences; import com.android.email.R; import com.android.email.RefreshManager; import com.android.email.activity.setup.AccountSecurity; +import com.android.email.activity.setup.AccountSettingsXL; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.Mailbox; +import com.android.emailcommon.utility.EmailAsyncTask; import android.app.ActionBar; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; +import android.content.Loader; +import android.database.Cursor; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.TextView; @@ -40,8 +50,7 @@ import java.util.ArrayList; import java.util.Set; /** - * A class manages what are showing on {@link MessageListXL} (i.e. account id, mailbox id, and - * message id), and show/hide fragments accordingly. + * UI Controller for x-large devices. Supports a multi-pane layout. * * Note: Always use {@link #commitFragmentTransaction} to commit fragment transactions. Currently * we use synchronous transactions only, but we may want to switch back to asynchronous later. @@ -49,7 +58,7 @@ import java.util.Set; * TODO: Test it. It's testable if we implement MockFragmentTransaction, which may be too early * to do so at this point. (API may not be stable enough yet.) * - * TODO Refine "move to". + * TODO Consider extracting out a separate class to manage the action bar */ class UIControllerTwoPane implements MailboxFinder.Callback, @@ -57,9 +66,15 @@ class UIControllerTwoPane implements MailboxListFragment.Callback, MessageListFragment.Callback, MessageViewFragment.Callback { - private static final String BUNDLE_KEY_ACCOUNT_ID = "MessageListXl.state.account_id"; - private static final String BUNDLE_KEY_MAILBOX_ID = "MessageListXl.state.mailbox_id"; - private static final String BUNDLE_KEY_MESSAGE_ID = "MessageListXl.state.message_id"; + private static final String BUNDLE_KEY_ACCOUNT_ID = "UIControllerTwoPane.state.account_id"; + private static final String BUNDLE_KEY_MAILBOX_ID = "UIControllerTwoPane.state.mailbox_id"; + private static final String BUNDLE_KEY_MESSAGE_ID = "UIControllerTwoPane.state.message_id"; + + /* package */ static final int MAILBOX_REFRESH_MIN_INTERVAL = 30 * 1000; // in milliseconds + /* package */ static final int INBOX_AUTO_REFRESH_MIN_INTERVAL = 10 * 1000; // in milliseconds + + private static final int LOADER_ID_ACCOUNT_LIST + = EmailActivity.UI_CONTROLLER_LOADER_ID_BASE + 0; /** No account selected */ static final long NO_ACCOUNT = -1; @@ -76,11 +91,16 @@ class UIControllerTwoPane implements /** Current message id */ private long mMessageId = NO_MESSAGE; - // UI elements + // Action bar private ActionBar mActionBar; + private AccountSelectorAdapter mAccountsSelectorAdapter; + private final ActionBarNavigationCallback mActionBarNavigationCallback = + new ActionBarNavigationCallback(); private View mActionBarMailboxNameView; private TextView mActionBarMailboxName; private TextView mActionBarUnreadCount; + + // Other UI elements private ThreePaneLayout mThreePane; /** @@ -101,7 +121,7 @@ class UIControllerTwoPane implements private MailboxFinder mMailboxFinder; - private RefreshManager mRefreshManager; + private final RefreshManager mRefreshManager; private MessageOrderManager mOrderManager; private final MessageOrderManagerCallback mMessageOrderManagerCallback = new MessageOrderManagerCallback(); @@ -120,9 +140,11 @@ class UIControllerTwoPane implements private boolean mHoldFragmentInstallation = true; /** The owner activity */ - private final MessageListXL mActivity; + private final EmailActivity mActivity; - public UIControllerTwoPane(MessageListXL activity) { + private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); + + public UIControllerTwoPane(EmailActivity activity) { mActivity = activity; mRefreshManager = RefreshManager.getInstance(mActivity); } @@ -233,7 +255,11 @@ class UIControllerTwoPane implements @Override public void onAccountSelected(long accountId) { - openAccount(accountId); + // TODO openAccount should do the check eventually, but it's necessary for now. + if (accountId != getUIAccountId()) { + openAccount(accountId); + loadAccounts(); // update account spinner + } } @Override @@ -476,10 +502,17 @@ class UIControllerTwoPane implements return -1 != getActualAccountId(); } + /** + * Called by the host activity at the end of {@link Activity#onCreate}. + */ + public void onActivityCreated() { + loadAccounts(); + } + /** * Install all the fragments kept in {@link #mRestoredFragments}. * - * Must be called at the end of {@link MessageListXL#onCreate}. + * Must be called at the end of {@link EmailActivity#onCreate}. */ public void installRestoredFragments() { mHoldFragmentInstallation = false; @@ -492,7 +525,7 @@ class UIControllerTwoPane implements } /** - * Called by {@link MessageListXL} when a {@link Fragment} is attached. + * Called by {@link EmailActivity} when a {@link Fragment} is attached. * * If the activity has already been created, we initialize the fragment here. Otherwise we * keep the fragment in {@link #mRestoredFragments} and initialize it after the activity's @@ -508,7 +541,7 @@ class UIControllerTwoPane implements } /** - * Called from {@link MessageListXL#onStart}. + * Called from {@link EmailActivity#onStart}. */ public void onStart() { if (isMessageSelected()) { @@ -517,30 +550,31 @@ class UIControllerTwoPane implements } /** - * Called from {@link MessageListXL#onResume}. + * Called from {@link EmailActivity#onResume}. */ public void onResume() { updateActionBar(); } /** - * Called from {@link MessageListXL#onPause}. + * Called from {@link EmailActivity#onPause}. */ public void onPause() { } /** - * Called from {@link MessageListXL#onStop}. + * Called from {@link EmailActivity#onStop}. */ public void onStop() { stopMessageOrderManager(); } /** - * Called from {@link MessageListXL#onDestroy}. + * Called from {@link EmailActivity#onDestroy}. */ public void onDestroy() { mHoldFragmentInstallation = true; // No more fragment installation. + mTaskTracker.cancellAllInterrupt(); closeMailboxFinder(); } @@ -729,17 +763,7 @@ class UIControllerTwoPane implements if (changeVisiblePane) { mThreePane.showLeftPane(); } - mActivity.onAccountChanged(mAccountId); - } - - /** - * Handles the back event. - * - * @param isSystemBackKey See {@link ThreePaneLayout#onBackPressed} - * @return true if the event is handled. - */ - public boolean onBackPressed(boolean isSystemBackKey) { - return mThreePane.onBackPressed(isSystemBackKey); + mActivity.updateRefreshProgress(); } /** @@ -930,4 +954,282 @@ class UIControllerTwoPane implements } return false; } + + /** + * Load account list for the action bar. + * + * If there's only one account configured, show the account name in the action bar. + * If more than one account are configured, show a spinner in the action bar, and select the + * current account. + */ + private void loadAccounts() { + if (mAccountsSelectorAdapter == null) { + mAccountsSelectorAdapter = new AccountSelectorAdapter(mActivity); + } + mActivity.getLoaderManager().initLoader(LOADER_ID_ACCOUNT_LIST, null, + new LoaderCallbacks() { + @Override + public Loader onCreateLoader(int id, Bundle args) { + return AccountSelectorAdapter.createLoader(mActivity); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + updateAccountList(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAccountsSelectorAdapter.swapCursor(null); + } + }); + } + + /** + * Called when the LOADER_ID_ACCOUNT_LIST loader loads the data. Update the account spinner + * on the action bar. + */ + private void updateAccountList(Cursor accountsCursor) { + final int count = accountsCursor.getCount(); + if (count == 0) { + // Open Welcome, which in turn shows the adding a new account screen. + Welcome.actionStart(mActivity); + mActivity.finish(); + return; + } + + // If ony one acount, don't show dropdown. + final ActionBar ab = mActionBar; + if (count == 1) { + accountsCursor.moveToFirst(); + + // Show the account name as the title. + ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE); + ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); + ab.setTitle(AccountSelectorAdapter.getAccountDisplayName(accountsCursor)); + return; + } + + // Find the currently selected account, and select it. + int defaultSelection = 0; + if (isAccountSelected()) { + accountsCursor.moveToPosition(-1); + int i = 0; + while (accountsCursor.moveToNext()) { + final long accountId = AccountSelectorAdapter.getAccountId(accountsCursor); + if (accountId == getUIAccountId()) { + defaultSelection = i; + break; + } + i++; + } + } + + // Update the dropdown list. + mAccountsSelectorAdapter.swapCursor(accountsCursor); + + // Don't show the title. + ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); + ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + ab.setListNavigationCallbacks(mAccountsSelectorAdapter, mActionBarNavigationCallback); + ab.setSelectedNavigationItem(defaultSelection); + } + + private class ActionBarNavigationCallback implements ActionBar.OnNavigationListener { + @Override + public boolean onNavigationItemSelected(int itemPosition, long accountId) { + // TODO openAccount should do the check eventually, but it's necessary for now. + if (accountId != getUIAccountId()) { + openAccount(accountId); + } + return true; + } + } + + /** + * Handles {@link android.app.Activity#onCreateOptionsMenu} callback. + */ + public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { + inflater.inflate(R.menu.message_list_xl_option, menu); + return true; + } + + /** + * Handles {@link android.app.Activity#onPrepareOptionsMenu} callback. + */ + public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { + ActivityHelper.updateRefreshMenuIcon(menu.findItem(R.id.refresh), + isRefreshEnabled(), + isRefreshInProgress()); + return true; + } + + /** + * Handles {@link android.app.Activity#onOptionsItemSelected} callback. + * + * @return true if the option item is handled. + */ + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // Comes from the action bar when the app icon on the left is pressed. + // It works like a back press, but it won't close the activity. + return onBackPressed(false); + case R.id.compose: + return onCompose(); + case R.id.refresh: + onRefresh(); + return true; + case R.id.account_settings: + return onAccountSettings(); + } + return false; + } + + /** + * Performs the back action. + * + * @param isSystemBackKey true if the system back key was pressed. + * true if it's caused by the "home" icon click on the action bar. + */ + public boolean onBackPressed(boolean isSystemBackKey) { + if (mThreePane.onBackPressed(isSystemBackKey)) { + return true; + } + return false; + } + + /** + * Handles the "Compose" option item. Opens the message compose activity. + */ + private boolean onCompose() { + if (!isAccountSelected()) { + return false; // this shouldn't really happen + } + MessageCompose.actionCompose(mActivity, getActualAccountId()); + return true; + } + + /** + * Handles the "Compose" option item. Opens the settings activity. + */ + private boolean onAccountSettings() { + AccountSettingsXL.actionSettings(mActivity, getActualAccountId()); + return true; + } + + /** + * Handles the "refresh" option item. Opens the settings activity. + */ + // TODO used by experimental code in the activity -- otherwise can be private. + public void onRefresh() { + // Cancel previously running instance if any. + new RefreshTask(mTaskTracker, mActivity, getActualAccountId(), + getMailboxId()).cancelPreviousAndExecuteParallel(); + } + + /** + * Class to handle refresh. + * + * When the user press "refresh", + *
    + *
  • Refresh the current mailbox, if it's refreshable. (e.g. don't refresh combined inbox, + * drafts, etc. + *
  • Refresh the mailbox list, if it hasn't been refreshed in the last + * {@link #MAILBOX_REFRESH_MIN_INTERVAL}. + *
  • Refresh inbox, if it's not the current mailbox and it hasn't been refreshed in the last + * {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. + *
+ */ + /* package */ static class RefreshTask extends EmailAsyncTask { + private final Clock mClock; + private final Context mContext; + private final long mAccountId; + private final long mMailboxId; + private final RefreshManager mRefreshManager; + /* package */ long mInboxId; + + public RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, + long mailboxId) { + this(tracker, context, accountId, mailboxId, Clock.INSTANCE, + RefreshManager.getInstance(context)); + } + + /* package */ RefreshTask(EmailAsyncTask.Tracker tracker, Context context, long accountId, + long mailboxId, Clock clock, RefreshManager refreshManager) { + super(tracker); + mClock = clock; + mContext = context; + mRefreshManager = refreshManager; + mAccountId = accountId; + mMailboxId = mailboxId; + } + + /** + * Do DB access on a worker thread. + */ + @Override + protected Boolean doInBackground(Void... params) { + mInboxId = Account.getInboxId(mContext, mAccountId); + return Mailbox.isRefreshable(mContext, mMailboxId); + } + + /** + * Do the actual refresh. + */ + @Override + protected void onPostExecute(Boolean isCurrentMailboxRefreshable) { + if (isCancelled() || isCurrentMailboxRefreshable == null) { + return; + } + if (isCurrentMailboxRefreshable) { + mRefreshManager.refreshMessageList(mAccountId, mMailboxId, false); + } + // Refresh mailbox list + if (mAccountId != -1) { + if (shouldRefreshMailboxList()) { + mRefreshManager.refreshMailboxList(mAccountId); + } + } + // Refresh inbox + if (shouldAutoRefreshInbox()) { + mRefreshManager.refreshMessageList(mAccountId, mInboxId, false); + } + } + + /** + * @return true if the mailbox list of the current account hasn't been refreshed + * in the last {@link #MAILBOX_REFRESH_MIN_INTERVAL}. + */ + /* package */ boolean shouldRefreshMailboxList() { + if (mRefreshManager.isMailboxListRefreshing(mAccountId)) { + return false; + } + final long nextRefreshTime = mRefreshManager.getLastMailboxListRefreshTime(mAccountId) + + MAILBOX_REFRESH_MIN_INTERVAL; + if (nextRefreshTime > mClock.getTime()) { + return false; + } + return true; + } + + /** + * @return true if the inbox of the current account hasn't been refreshed + * in the last {@link #INBOX_AUTO_REFRESH_MIN_INTERVAL}. + */ + /* package */ boolean shouldAutoRefreshInbox() { + if (mInboxId == mMailboxId) { + return false; // Current ID == inbox. No need to auto-refresh. + } + if (mRefreshManager.isMessageListRefreshing(mInboxId)) { + return false; + } + final long nextRefreshTime = mRefreshManager.getLastMessageListRefreshTime(mInboxId) + + INBOX_AUTO_REFRESH_MIN_INTERVAL; + if (nextRefreshTime > mClock.getTime()) { + return false; + } + return true; + } + } } diff --git a/src/com/android/email/activity/Welcome.java b/src/com/android/email/activity/Welcome.java index 5719e2304..a145809d2 100644 --- a/src/com/android/email/activity/Welcome.java +++ b/src/com/android/email/activity/Welcome.java @@ -244,12 +244,12 @@ public class Welcome extends Activity { if (useTwoPane) { if (isMessageSelected()) { - MessageListXL.actionOpenMessage(mFromActivity, accountId, mMailboxId, + EmailActivity.actionOpenMessage(mFromActivity, accountId, mMailboxId, mMessageId); } else if (isMailboxSelected()) { - MessageListXL.actionOpenMailbox(mFromActivity, accountId, mMailboxId); + EmailActivity.actionOpenMailbox(mFromActivity, accountId, mMailboxId); } else { - MessageListXL.actionOpenAccount(mFromActivity, accountId); + EmailActivity.actionOpenAccount(mFromActivity, accountId); } } else { if (isMessageSelected()) { diff --git a/tests/src/com/android/email/activity/MessageListXLRefreshTaskTest.java b/tests/src/com/android/email/activity/UIControllerTwoPaneRefreshTaskTest.java similarity index 91% rename from tests/src/com/android/email/activity/MessageListXLRefreshTaskTest.java rename to tests/src/com/android/email/activity/UIControllerTwoPaneRefreshTaskTest.java index ec48d453a..1249861c4 100644 --- a/tests/src/com/android/email/activity/MessageListXLRefreshTaskTest.java +++ b/tests/src/com/android/email/activity/UIControllerTwoPaneRefreshTaskTest.java @@ -29,14 +29,14 @@ import android.test.suitebuilder.annotation.SmallTest; import junit.framework.Assert; /** - * Tests for {@link MessageListXL.RefreshTask}. + * Tests for {@link UIControllerTwoPane.RefreshTask}. * * TOOD Add more tests. * Right now, it only has tests for the "shouldXxx" methods, because it's hard to notice when * they're subtly broken. (And the spec may change.) */ @SmallTest -public class MessageListXLRefreshTaskTest extends AndroidTestCase { +public class UIControllerTwoPaneRefreshTaskTest extends AndroidTestCase { private MockClock mClock = new MockClock(); private MockRefreshManager mRefreshManager; @@ -57,8 +57,8 @@ public class MessageListXLRefreshTaskTest extends AndroidTestCase { final long ACCOUNT_ID = 5; final long MAILBOX_ID = 10; - MessageListXL.RefreshTask task = new MessageListXL.RefreshTask(null, getContext(), - ACCOUNT_ID, MAILBOX_ID, mClock, mRefreshManager); + UIControllerTwoPane.RefreshTask task = new UIControllerTwoPane.RefreshTask(null, + getContext(), ACCOUNT_ID, MAILBOX_ID, mClock, mRefreshManager); mRefreshManager.mExpectedAccountId = ACCOUNT_ID; @@ -88,7 +88,7 @@ public class MessageListXLRefreshTaskTest extends AndroidTestCase { // Not refreshing, refreshed TIMEOUT-1 ago == should NOT sync mRefreshManager.mLastMailboxListRefresTime = 1234567890; mClock.mTime = mRefreshManager.mLastMailboxListRefresTime - + MessageListXL.MAILBOX_REFRESH_MIN_INTERVAL - 1; + + UIControllerTwoPane.MAILBOX_REFRESH_MIN_INTERVAL - 1; assertFalse(task.shouldRefreshMailboxList()); // 1 ms laster... should sync. @@ -100,8 +100,8 @@ public class MessageListXLRefreshTaskTest extends AndroidTestCase { final long ACCOUNT_ID = 5; final long MAILBOX_ID = 10; - MessageListXL.RefreshTask task = new MessageListXL.RefreshTask(null, getContext(), - ACCOUNT_ID, MAILBOX_ID, mClock, mRefreshManager); + UIControllerTwoPane.RefreshTask task = new UIControllerTwoPane.RefreshTask(null, + getContext(), ACCOUNT_ID, MAILBOX_ID, mClock, mRefreshManager); mRefreshManager.mExpectedAccountId = ACCOUNT_ID; @@ -142,7 +142,7 @@ public class MessageListXLRefreshTaskTest extends AndroidTestCase { // Not refreshing, refreshed TIMEOUT-1 ago == should NOT sync mRefreshManager.mLastMessageListRefresTime = 1234567890; mClock.mTime = mRefreshManager.mLastMessageListRefresTime - + MessageListXL.INBOX_AUTO_REFRESH_MIN_INTERVAL - 1; + + UIControllerTwoPane.INBOX_AUTO_REFRESH_MIN_INTERVAL - 1; assertFalse(task.shouldAutoRefreshInbox()); // 1 ms laster... should sync.