Welcome.java revision 2736c1a11ce3ecdcd9d19aa9c324fb9ce0910c7b
1/*
2 * Copyright (C) 2008 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 android.app.Activity;
20import android.content.Context;
21import android.content.Intent;
22import android.net.Uri;
23import android.os.Bundle;
24import android.text.TextUtils;
25import android.util.Log;
26import android.view.LayoutInflater;
27import android.view.Menu;
28import android.view.MenuItem;
29import android.view.View;
30import android.view.ViewGroup.LayoutParams;
31
32import com.android.email.Email;
33import com.android.email.Preferences;
34import com.android.email.R;
35import com.android.email.activity.setup.AccountSettings;
36import com.android.email.activity.setup.AccountSetupBasics;
37import com.android.email.service.EmailServiceUtils;
38import com.android.email.service.MailService;
39import com.android.emailcommon.Logging;
40import com.android.emailcommon.provider.Account;
41import com.android.emailcommon.provider.EmailContent;
42import com.android.emailcommon.provider.EmailContent.Message;
43import com.android.emailcommon.provider.Mailbox;
44import com.android.emailcommon.provider.Policy;
45import com.android.emailcommon.utility.EmailAsyncTask;
46import com.android.emailcommon.utility.IntentUtilities;
47import com.android.emailcommon.utility.Utility;
48import com.google.common.annotations.VisibleForTesting;
49
50/**
51 * The Welcome activity initializes the application and starts {@link EmailActivity}, or launch
52 * {@link AccountSetupBasics} if no accounts are configured.
53 *
54 * TOOD Show "your messages are on the way" message like gmail does during the inbox lookup.
55 */
56public class Welcome extends Activity {
57    /*
58     * Commands for testing...
59     *  Open 1 pane
60        adb shell am start -a android.intent.action.MAIN \
61            -d '"content://ui.email.android.com/view/mailbox"' \
62            -e DEBUG_PANE_MODE 1
63
64     *  Open 2 pane
65        adb shell am start -a android.intent.action.MAIN \
66            -d '"content://ui.email.android.com/view/mailbox"' \
67            -e DEBUG_PANE_MODE 2
68
69     *  Open an account (ID=1) in 2 pane
70        adb shell am start -a android.intent.action.MAIN \
71            -d '"content://ui.email.android.com/view/mailbox?ACCOUNT_ID=1"' \
72            -e DEBUG_PANE_MODE 2
73
74     *  Open a message (account id=1, mailbox id=2, message id=3)
75        adb shell am start -a android.intent.action.MAIN \
76            -d '"content://ui.email.android.com/view/mailbox?ACCOUNT_ID=1&MAILBOX_ID=2&MESSAGE_ID=3"' \
77            -e DEBUG_PANE_MODE 2
78
79     *  Open the combined starred on the combined view
80        adb shell am start -a android.intent.action.MAIN \
81            -d '"content://ui.email.android.com/view/mailbox?ACCOUNT_ID=1152921504606846976&MAILBOX_ID=-4"' \
82            -e DEBUG_PANE_MODE 2
83     */
84
85    /**
86     * Extra for debugging.  Set 1 to force one-pane.  Set 2 to force two-pane.
87     */
88    private static final String EXTRA_DEBUG_PANE_MODE = "DEBUG_PANE_MODE";
89
90    private static final String VIEW_MAILBOX_INTENT_URL_PATH = "/view/mailbox";
91
92    private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker();
93
94    private View mWaitingForSyncView;
95
96    private long mAccountId;
97    private long mMailboxId;
98    private long mMessageId;
99    private String mAccountUuid;
100
101    private MailboxFinder mInboxFinder;
102
103    /**
104     * Launch this activity.  Note:  It's assumed that this activity is only called as a means to
105     * 'reset' the UI state; Because of this, it is always launched with FLAG_ACTIVITY_CLEAR_TOP,
106     * which will drop any other activities on the stack (e.g. AccountFolderList or MessageList).
107     */
108    public static void actionStart(Activity fromActivity) {
109        Intent i = IntentUtilities.createRestartAppIntent(fromActivity, Welcome.class);
110        fromActivity.startActivity(i);
111    }
112
113    /**
114     * Create an Intent to open email activity. If <code>accountId</code> is not -1, the
115     * specified account will be automatically be opened when the activity starts.
116     */
117    public static Intent createOpenAccountInboxIntent(Context context, long accountId) {
118        final Uri.Builder b = IntentUtilities.createActivityIntentUrlBuilder(
119                VIEW_MAILBOX_INTENT_URL_PATH);
120        IntentUtilities.setAccountId(b, accountId);
121        return IntentUtilities.createRestartAppIntent(b.build());
122    }
123
124    /**
125     * Create an Intent to open a message.
126     */
127    public static Intent createOpenMessageIntent(Context context, long accountId,
128            long mailboxId, long messageId) {
129        final Uri.Builder b = IntentUtilities.createActivityIntentUrlBuilder(
130                VIEW_MAILBOX_INTENT_URL_PATH);
131        IntentUtilities.setAccountId(b, accountId);
132        IntentUtilities.setMailboxId(b, mailboxId);
133        IntentUtilities.setMessageId(b, messageId);
134        return IntentUtilities.createRestartAppIntent(b.build());
135    }
136
137    /**
138     * Open account's inbox.
139     */
140    public static void actionOpenAccountInbox(Activity fromActivity, long accountId) {
141        fromActivity.startActivity(createOpenAccountInboxIntent(fromActivity, accountId));
142    }
143
144    /**
145     * Create an {@link Intent} for account shortcuts.  The returned intent stores the account's
146     * UUID rather than the account ID, which will be changed after account restore.
147     */
148    public static Intent createAccountShortcutIntent(Context context, String uuid, long mailboxId) {
149        final Uri.Builder b = IntentUtilities.createActivityIntentUrlBuilder(
150                VIEW_MAILBOX_INTENT_URL_PATH);
151        IntentUtilities.setAccountUuid(b, uuid);
152        IntentUtilities.setMailboxId(b, mailboxId);
153        return IntentUtilities.createRestartAppIntent(b.build());
154    }
155
156    /**
157     * If the {@link #EXTRA_DEBUG_PANE_MODE} extra is "1" or "2", return 1 or 2 respectively.
158     * Otherwise return 0.
159     *
160     * @see UiUtilities#setDebugPaneMode(int)
161     * @see UiUtilities#useTwoPane(Context)
162     */
163    private static int getDebugPaneMode(Intent i) {
164        Bundle extras = i.getExtras();
165        if (extras != null) {
166            String s = extras.getString(EXTRA_DEBUG_PANE_MODE);
167            if ("1".equals(s)) {
168                return 1;
169            } else if ("2".equals(s)) {
170                return 2;
171            }
172        }
173        return 0;
174    }
175
176    @Override
177    public void onCreate(Bundle icicle) {
178        super.onCreate(icicle);
179        ActivityHelper.debugSetWindowFlags(this);
180
181        // Because the app could be reloaded (for debugging, etc.), we need to make sure that
182        // ExchangeService gets a chance to start.  There is no harm to starting it if it has
183        // already been started
184        // When the service starts, it reconciles EAS accounts.
185        // TODO More completely separate ExchangeService from Email app
186        EmailServiceUtils.startExchangeService(this);
187
188        // Extract parameters from the intent.
189        final Intent intent = getIntent();
190        mAccountId = IntentUtilities.getAccountIdFromIntent(intent);
191        mMailboxId = IntentUtilities.getMailboxIdFromIntent(intent);
192        mMessageId = IntentUtilities.getMessageIdFromIntent(intent);
193        mAccountUuid = IntentUtilities.getAccountUuidFromIntent(intent);
194        UiUtilities.setDebugPaneMode(getDebugPaneMode(intent));
195
196        // Reconcile POP/IMAP accounts.  EAS accounts are taken care of by ExchangeService.
197        if (MailService.hasMismatchInPopImapAccounts(this)) {
198            EmailAsyncTask.runAsyncParallel(new Runnable() {
199                @Override
200                public void run() {
201                    // Reconciling can be heavy - so do it in the background.
202                    MailService.reconcilePopImapAccountsSync(Welcome.this);
203                    resolveAccount();
204                }
205            });
206        } else {
207            resolveAccount();
208        }
209
210        // Reset the "accounts changed" notification, now that we're here
211        Email.setNotifyUiAccountsChanged(false);
212    }
213
214    @Override
215    public boolean onCreateOptionsMenu(Menu menu) {
216        // Only create the menu if we had to stop and show a loading spinner - otherwise
217        // this is a transient activity with no UI.
218        if (mInboxFinder == null) {
219            return super.onCreateOptionsMenu(menu);
220        }
221
222        getMenuInflater().inflate(R.menu.welcome, menu);
223        return true;
224    }
225
226    @Override
227    public boolean onOptionsItemSelected(MenuItem item) {
228        if (item.getItemId() == R.id.account_settings) {
229            AccountSettings.actionSettings(this, mAccountId);
230            return true;
231        }
232        return super.onOptionsItemSelected(item);
233    }
234
235    @Override
236    protected void onStop() {
237        // Cancel all running tasks.
238        // (If it's stopping for configuration changes, we just re-do everything on the new
239        // instance)
240        stopInboxLookup();
241        mTaskTracker.cancellAllInterrupt();
242
243        super.onStop();
244
245        if (!isChangingConfigurations()) {
246            // This means the user opened some other app.
247            // Just close self and not launch EmailActivity.
248            if (Email.DEBUG && Logging.DEBUG_LIFECYCLE) {
249                Log.d(Logging.LOG_TAG, "Welcome: Closing self...");
250            }
251            finish();
252        }
253    }
254
255    /**
256     * {@inheritDoc}
257     *
258     * When launching an activity from {@link Welcome}, we always want to set
259     * {@link Intent#FLAG_ACTIVITY_FORWARD_RESULT}.
260     */
261    @Override
262    public void startActivity(Intent intent) {
263        intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
264        super.startActivity(intent);
265    }
266
267    /**
268     * Stop inbox lookup.  This MSUT be called on the UI thread.
269     */
270    private void stopInboxLookup() {
271        if (mInboxFinder != null) {
272            mInboxFinder.cancel();
273            mInboxFinder = null;
274        }
275    }
276
277    /**
278     * Start inbox lookup.  This MSUT be called on the UI thread.
279     */
280    private void startInboxLookup() {
281        Log.i(Logging.LOG_TAG, "Inbox not found.  Starting mailbox finder...");
282        stopInboxLookup(); // Stop if already running -- it shouldn't be but just in case.
283        mInboxFinder = new MailboxFinder(this, mAccountId, Mailbox.TYPE_INBOX,
284                mMailboxFinderCallback);
285        mInboxFinder.startLookup();
286
287        // Show "your email will appear shortly" message.
288        mWaitingForSyncView = LayoutInflater.from(this).inflate(
289                R.layout.waiting_for_sync_message, null);
290        addContentView(mWaitingForSyncView, new LayoutParams(
291                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
292        invalidateOptionsMenu();
293    }
294
295    /**
296     * Determine which account to open with the given account ID and UUID.
297     *
298     * @return ID of the account to use.
299     */
300    @VisibleForTesting
301    static long resolveAccountId(Context context, long inputAccountId, String inputUuid) {
302        final long accountId;
303
304        if (!TextUtils.isEmpty(inputUuid)) {
305            // If a UUID is specified, try to use it.
306            // If the UUID is invalid, accountId will be NO_ACCOUNT.
307            accountId = Account.getAccountIdFromUuid(context, inputUuid);
308
309        } else if (inputAccountId != Account.NO_ACCOUNT) {
310            // If a valid account ID is specified, just use it.
311            if (inputAccountId == Account.ACCOUNT_ID_COMBINED_VIEW
312                    || Account.isValidId(context, inputAccountId)) {
313                accountId = inputAccountId;
314            } else {
315                accountId = Account.NO_ACCOUNT;
316            }
317        } else {
318            // Neither an accountID or a UUID is specified.
319            // Use the last account used, falling back to the default.
320            long lastUsedId = Preferences.getPreferences(context).getLastUsedAccountId();
321            if (lastUsedId != Account.NO_ACCOUNT) {
322                if (!Account.isValidId(context, lastUsedId)) {
323                    // The last account that was used has since been deleted.
324                    lastUsedId = Account.NO_ACCOUNT;
325                    Preferences.getPreferences(context).setLastUsedAccountId(Account.NO_ACCOUNT);
326                }
327            }
328            accountId = (lastUsedId == Account.NO_ACCOUNT)
329                    ? Account.getDefaultAccountId(context)
330                    : lastUsedId;
331        }
332        if (accountId != Account.NO_ACCOUNT) {
333            // Okay, the given account is valid.
334            return accountId;
335        } else {
336            // No, it's invalid.  Show the warning toast and use the default.
337            Utility.showToast(context, R.string.toast_account_not_found);
338            return Account.getDefaultAccountId(context);
339        }
340    }
341
342    /**
343     * Determine which account to use according to the number of accounts already set up,
344     * {@link #mAccountId} and {@link #mAccountUuid}.
345     *
346     * <pre>
347     * 1. If there's no account configured, start account setup.
348     * 2. Otherwise detemine which account to open with {@link #resolveAccountId} and
349     *   2a. If the account doesn't have inbox yet, start inbox finder.
350     *   2b. Otherwise open the main activity.
351     * </pre>
352     */
353    private void resolveAccount() {
354        final int numAccount = EmailContent.count(this, Account.CONTENT_URI);
355        if (numAccount == 0) {
356            AccountSetupBasics.actionNewAccount(this);
357            finish();
358            return;
359        } else {
360            mAccountId = resolveAccountId(this, mAccountId, mAccountUuid);
361            if (Account.isNormalAccount(mAccountId) &&
362                    Mailbox.findMailboxOfType(this, mAccountId, Mailbox.TYPE_INBOX)
363                            == Mailbox.NO_MAILBOX) {
364                startInboxLookup();
365                return;
366            }
367        }
368        startEmailActivity();
369    }
370
371    /**
372     * Start {@link EmailActivity} using {@link #mAccountId}, {@link #mMailboxId} and
373     * {@link #mMessageId}.
374     */
375    private void startEmailActivity() {
376        final Intent i;
377        if (mMessageId != Message.NO_MESSAGE) {
378            i = EmailActivity.createOpenMessageIntent(this, mAccountId, mMailboxId, mMessageId);
379        } else if (mMailboxId != Mailbox.NO_MAILBOX) {
380            i = EmailActivity.createOpenMailboxIntent(this, mAccountId, mMailboxId);
381        } else {
382            i = EmailActivity.createOpenAccountIntent(this, mAccountId);
383        }
384        startActivity(i);
385        finish();
386    }
387
388    private final MailboxFinder.Callback mMailboxFinderCallback = new MailboxFinder.Callback() {
389        // This MUST be called from callback methods.
390        private void cleanUp() {
391            mInboxFinder = null;
392        }
393
394        @Override
395        public void onAccountNotFound() {
396            cleanUp();
397            // Account removed?  Clear the IDs and restart the task.  Which will result in either
398            // a) show account setup if there's really no accounts  or b) open the default account.
399
400            mAccountId = Account.NO_ACCOUNT;
401            mMailboxId = Mailbox.NO_MAILBOX;
402            mMessageId = Message.NO_MESSAGE;
403            mAccountUuid = null;
404
405            // Restart the account resolution.
406            resolveAccount();
407        }
408
409        @Override
410        public void onMailboxNotFound(long accountId) {
411            // Just do the same thing as "account not found".
412            onAccountNotFound();
413        }
414
415        @Override
416        public void onAccountSecurityHold(long accountId) {
417            cleanUp();
418            // If we can't find the account, we know what to do
419            Account account = Account.restoreAccountWithId(Welcome.this, accountId);
420            if (account == null) {
421                onAccountNotFound();
422                return;
423            }
424            // If there's no policy or it's "unsupported", act like the account doesn't exist
425            Policy policy = Policy.restorePolicyWithId(Welcome.this, account.mPolicyKey);
426            if (policy == null || (policy.mProtocolPoliciesUnsupported != null)) {
427                onAccountNotFound();
428                return;
429            }
430            // Otherwise, try advancing security
431            ActivityHelper.showSecurityHoldDialog(Welcome.this, accountId);
432            finish();
433        }
434
435        @Override
436        public void onMailboxFound(long accountId, long mailboxId) {
437            cleanUp();
438
439            // Okay the account has Inbox now.  Start the main activity.
440            startEmailActivity();
441        }
442    };
443}
444