UIControllerBase.java revision 147e41d00aed3eac469567c4c7f50616ef3df994
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity;
18
19import com.android.email.Email;
20import com.android.email.R;
21import com.android.email.RefreshManager;
22import com.android.email.activity.setup.AccountSettings;
23import com.android.emailcommon.Logging;
24import com.android.emailcommon.provider.EmailContent.Account;
25import com.android.emailcommon.provider.EmailContent.Message;
26import com.android.emailcommon.provider.Mailbox;
27import com.android.emailcommon.utility.EmailAsyncTask;
28
29import android.app.Fragment;
30import android.app.FragmentManager;
31import android.app.FragmentTransaction;
32import android.content.Context;
33import android.os.Bundle;
34import android.util.Log;
35import android.view.Menu;
36import android.view.MenuInflater;
37import android.view.MenuItem;
38
39import java.util.ArrayList;
40
41/**
42 * Base class for the UI controller.
43 *
44 * Note: Always use {@link #commitFragmentTransaction} and {@link #popBackStack} to operate fragment
45 * transactions.
46 * (Currently we use synchronous transactions only, but we may want to switch back to asynchronous
47 * later.)
48 */
49abstract class UIControllerBase {
50    protected static final String BUNDLE_KEY_ACCOUNT_ID = "UIController.state.account_id";
51    protected static final String BUNDLE_KEY_MAILBOX_ID = "UIController.state.mailbox_id";
52    protected static final String BUNDLE_KEY_MESSAGE_ID = "UIController.state.message_id";
53
54    /** The owner activity */
55    final EmailActivity mActivity;
56
57    final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
58
59    final RefreshManager mRefreshManager;
60
61    /**
62     * List of fragments that are restored by the framework while the activity is being re-created
63     * for configuration changes (e.g. screen rotation).  We'll install them later when the activity
64     * is created in {@link #installRestoredFragments()}.
65     */
66    private final ArrayList<Fragment> mRestoredFragments = new ArrayList<Fragment>();
67
68    /**
69     * Whether fragment installation should be hold.
70     * We hold installing fragments until {@link #installRestoredFragments()} is called.
71     */
72    private boolean mHoldFragmentInstallation = true;
73
74    private final RefreshManager.Listener mRefreshListener
75            = new RefreshManager.Listener() {
76        @Override
77        public void onMessagingError(final long accountId, long mailboxId, final String message) {
78            updateRefreshProgress();
79        }
80
81        @Override
82        public void onRefreshStatusChanged(long accountId, long mailboxId) {
83            updateRefreshProgress();
84        }
85    };
86
87    public UIControllerBase(EmailActivity activity) {
88        mActivity = activity;
89        mRefreshManager = RefreshManager.getInstance(mActivity);
90    }
91
92    /** @return the layout ID for the activity. */
93    public abstract int getLayoutId();
94
95    /**
96     * @return true if the UI controller currently can install fragments.
97     */
98    protected final boolean isFragmentInstallable() {
99        return !mHoldFragmentInstallation;
100    }
101
102    /**
103     * Must be called just after the activity sets up the content view.  Used to initialize views.
104     *
105     * (Due to the complexity regarding class/activity initialization order, we can't do this in
106     * the constructor.)
107     */
108    public void onActivityViewReady() {
109        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
110            Log.d(Logging.LOG_TAG, this + " onActivityViewReady");
111        }
112    }
113
114    /**
115     * Called at the end of {@link EmailActivity#onCreate}.
116     */
117    public void onActivityCreated() {
118        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
119            Log.d(Logging.LOG_TAG, this + " onActivityCreated");
120        }
121        mRefreshManager.registerListener(mRefreshListener);
122    }
123
124    /**
125     * Handles the {@link android.app.Activity#onStart} callback.
126     */
127    public void onActivityStart() {
128        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
129            Log.d(Logging.LOG_TAG, this + " onActivityStart");
130        }
131    }
132
133    /**
134     * Handles the {@link android.app.Activity#onResume} callback.
135     */
136    public void onActivityResume() {
137        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
138            Log.d(Logging.LOG_TAG, this + " onActivityResume");
139        }
140    }
141
142    /**
143     * Handles the {@link android.app.Activity#onPause} callback.
144     */
145    public void onActivityPause() {
146        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
147            Log.d(Logging.LOG_TAG, this + " onActivityPause");
148        }
149    }
150
151    /**
152     * Handles the {@link android.app.Activity#onStop} callback.
153     */
154    public void onActivityStop() {
155        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
156            Log.d(Logging.LOG_TAG, this + " onActivityStop");
157        }
158    }
159
160    /**
161     * Handles the {@link android.app.Activity#onDestroy} callback.
162     */
163    public void onActivityDestroy() {
164        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
165            Log.d(Logging.LOG_TAG, this + " onActivityDestroy");
166        }
167        mHoldFragmentInstallation = true; // No more fragment installation.
168        mRefreshManager.unregisterListener(mRefreshListener);
169        mTaskTracker.cancellAllInterrupt();
170    }
171
172    /**
173     * Install all the fragments kept in {@link #mRestoredFragments}.
174     *
175     * Must be called at the end of {@link EmailActivity#onCreate}.
176     */
177    public final void installRestoredFragments() {
178        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
179            Log.d(Logging.LOG_TAG, this + " installRestoredFragments");
180        }
181
182        mHoldFragmentInstallation = false;
183
184        // Install all the fragments restored by the framework.
185        for (Fragment fragment : mRestoredFragments) {
186            installFragment(fragment);
187        }
188        mRestoredFragments.clear();
189    }
190
191    /**
192     * Handles the {@link android.app.Activity#onSaveInstanceState} callback.
193     */
194    public void onSaveInstanceState(Bundle outState) {
195        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
196            Log.d(Logging.LOG_TAG, this + " onSaveInstanceState");
197        }
198    }
199
200    /**
201     * Handles the {@link android.app.Activity#onRestoreInstanceState} callback.
202     */
203    public void restoreInstanceState(Bundle savedInstanceState) {
204        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
205            Log.d(Logging.LOG_TAG, this + " restoreInstanceState");
206        }
207    }
208
209    /**
210     * Handles the {@link android.app.Activity#onAttachFragment} callback.
211     *
212     * If the activity has already been created, we initialize the fragment here.  Otherwise we
213     * keep the fragment in {@link #mRestoredFragments} and initialize it after the activity's
214     * onCreate.
215     */
216    public final void onAttachFragment(Fragment fragment) {
217        if (mHoldFragmentInstallation) {
218            // Fragment being restored by the framework during the activity recreation.
219            mRestoredFragments.add(fragment);
220            return;
221        }
222        installFragment(fragment);
223    }
224
225    private void installFragment(Fragment fragment) {
226        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
227            Log.d(Logging.LOG_TAG, this + " installFragment  fragment=" + fragment);
228        }
229        if (fragment instanceof MailboxListFragment) {
230            installMailboxListFragment((MailboxListFragment) fragment);
231        } else if (fragment instanceof MessageListFragment) {
232            installMessageListFragment((MessageListFragment) fragment);
233        } else if (fragment instanceof MessageViewFragment) {
234            installMessageViewFragment((MessageViewFragment) fragment);
235        } else {
236            // Ignore -- uninteresting fragments such as dialogs.
237        }
238    }
239
240    protected abstract void installMailboxListFragment(MailboxListFragment fragment);
241
242    protected abstract void installMessageListFragment(MessageListFragment fragment);
243
244    protected abstract void installMessageViewFragment(MessageViewFragment fragment);
245
246    // not used
247    protected final void popBackStack(FragmentManager fm, String name, int flags) {
248        fm.popBackStackImmediate(name, flags);
249    }
250
251    protected final void commitFragmentTransaction(FragmentTransaction ft) {
252        ft.commit();
253        mActivity.getFragmentManager().executePendingTransactions();
254    }
255
256    /**
257     * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
258     *
259     * @see #getActualAccountId()
260     */
261    public abstract long getUIAccountId();
262
263    /**
264     * @return true if an account is selected, or the current view is the combined view.
265     */
266    public final boolean isAccountSelected() {
267        return getUIAccountId() != Account.NO_ACCOUNT;
268    }
269
270    /**
271     * @return if an actual account is selected.  (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW}
272     * is not considered "actual".s)
273     */
274    public final boolean isActualAccountSelected() {
275        return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW);
276    }
277
278    /**
279     * @return the currently selected account ID.  If the current view is the combined view,
280     * it'll return {@link Account#NO_ACCOUNT}.
281     *
282     * @see #getUIAccountId()
283     */
284    public final long getActualAccountId() {
285        return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT;
286    }
287
288    /**
289     * Show the default view for the given account.
290     *
291     * No-op if the given account is already selected.
292     *
293     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
294     *     Must never be {@link Account#NO_ACCOUNT}.
295     */
296    public final void switchAccount(long accountId) {
297        if (accountId == getUIAccountId()) {
298            // Do nothing if the account is already selected.  Not even going back to the inbox.
299            return;
300        }
301        openAccount(accountId);
302    }
303
304    /**
305     * Shortcut for {@link #open} with {@link Mailbox#NO_MAILBOX} and {@link Message#NO_MESSAGE}.
306     */
307    protected final void openAccount(long accountId) {
308        open(accountId, Mailbox.NO_MAILBOX, Message.NO_MESSAGE);
309    }
310
311    /**
312     * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}.
313     */
314    protected final void openMailbox(long accountId, long mailboxId) {
315        open(accountId, mailboxId, Message.NO_MESSAGE);
316    }
317
318    /**
319     * Loads the given account and optionally selects the given mailbox and message.  Used to open
320     * a particular view at a request from outside of the activity, such as the widget.
321     *
322     * @param accountId ID of the account to load.  Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
323     *     Must never be {@link Account#NO_ACCOUNT}.
324     * @param mailboxId ID of the mailbox to load. If {@link Mailbox#NO_MAILBOX},
325     *     load the account's inbox.
326     * @param messageId ID of the message to load. If {@link Message#NO_MESSAGE},
327     *     do not open a message.
328     */
329    public abstract void open(long accountId, long mailboxId, long messageId);
330
331    /**
332     * Performs the back action.
333     *
334     * @param isSystemBackKey <code>true</code> if the system back key was pressed.
335     * <code>false</code> if it's caused by the "home" icon click on the action bar.
336     */
337    public abstract boolean onBackPressed(boolean isSystemBackKey);
338
339    /**
340     * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback.
341     */
342    public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
343        inflater.inflate(R.menu.email_activity_options, menu);
344        return true;
345    }
346
347    /**
348     * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback.
349     */
350    public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) {
351
352        // Update the refresh button.
353        MenuItem item = menu.findItem(R.id.refresh);
354        if (isRefreshEnabled()) {
355            item.setVisible(true);
356            if (isRefreshInProgress()) {
357                item.setActionView(R.layout.action_bar_indeterminate_progress);
358            } else {
359                item.setActionView(null);
360            }
361        } else {
362            item.setVisible(false);
363        }
364        return true;
365    }
366
367    /**
368     * Handles the {@link android.app.Activity#onOptionsItemSelected} callback.
369     *
370     * @return true if the option item is handled.
371     */
372    public boolean onOptionsItemSelected(MenuItem item) {
373        switch (item.getItemId()) {
374            case android.R.id.home:
375                // Comes from the action bar when the app icon on the left is pressed.
376                // It works like a back press, but it won't close the activity.
377                return onBackPressed(false);
378            case R.id.compose:
379                return onCompose();
380            case R.id.refresh:
381                onRefresh();
382                return true;
383            case R.id.account_settings:
384                return onAccountSettings();
385        }
386        return false;
387    }
388
389    /**
390     * Opens the message compose activity.
391     */
392    private boolean onCompose() {
393        if (!isAccountSelected()) {
394            return false; // this shouldn't really happen
395        }
396        MessageCompose.actionCompose(mActivity, getActualAccountId());
397        return true;
398    }
399
400    /**
401     * Handles the "Settings" option item.  Opens the settings activity.
402     */
403    private boolean onAccountSettings() {
404        AccountSettings.actionSettings(mActivity, getActualAccountId());
405        return true;
406    }
407
408    /**
409     * STOPSHIP For experimental UI.  Remove this.
410     *
411     * @return mailbox ID which we search for messages.
412     */
413    public abstract long getSearchMailboxId();
414
415    /**
416     * STOPSHIP For experimental UI.  Remove this.
417     *
418     * @return mailbox ID for "mailbox settings" option.
419     */
420    public abstract long getMailboxSettingsMailboxId();
421
422    /**
423     * STOPSHIP For experimental UI.  Make it abstract protected.
424     *
425     * Performs "refesh".
426     */
427    public abstract void onRefresh();
428
429    /**
430     * @return true if refresh is in progress for the current mailbox.
431     */
432    protected abstract boolean isRefreshInProgress();
433
434    /**
435     * @return true if the UI should enable the "refresh" command.
436     */
437    protected abstract boolean isRefreshEnabled();
438
439
440    /**
441     * Start/stop the "refresh" animation on the action bar according to the current refresh state.
442     *
443     * (We start the animation if {@link #isRefreshInProgress} returns true,
444     * and stop otherwise.)
445     */
446    protected void updateRefreshProgress() {
447        mActivity.invalidateOptionsMenu();
448    }
449}
450