AccountSetupIncoming.java revision 9d387ff0bb88ad952b53c956b468dcbcec248752
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.setup;
18
19import com.android.email.Account;
20import com.android.email.AccountBackupRestore;
21import com.android.email.R;
22import com.android.email.Utility;
23import com.android.email.provider.EmailContent;
24
25import android.app.Activity;
26import android.app.AlertDialog;
27import android.app.Dialog;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.os.Bundle;
31import android.text.Editable;
32import android.text.TextWatcher;
33import android.text.method.DigitsKeyListener;
34import android.view.View;
35import android.view.View.OnClickListener;
36import android.widget.AdapterView;
37import android.widget.ArrayAdapter;
38import android.widget.Button;
39import android.widget.EditText;
40import android.widget.Spinner;
41import android.widget.TextView;
42
43import java.net.URI;
44import java.net.URISyntaxException;
45
46public class AccountSetupIncoming extends Activity implements OnClickListener {
47    private static final String EXTRA_ACCOUNT = "account";
48    private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
49
50    private static final int POP_PORTS[] = {
51            110, 995, 995, 110, 110
52    };
53    private static final String POP_SCHEMES[] = {
54            "pop3", "pop3+ssl+", "pop3+ssl+trustallcerts", "pop3+tls+", "pop3+tls+trustallcerts"
55    };
56    private static final int IMAP_PORTS[] = {
57            143, 993, 993, 143, 143
58    };
59    private static final String IMAP_SCHEMES[] = {
60            "imap", "imap+ssl+", "imap+ssl+trustallcerts", "imap+tls+", "imap+tls+trustallcerts"
61    };
62
63    private final static int DIALOG_DUPLICATE_ACCOUNT = 1;
64
65    private int mAccountPorts[];
66    private String mAccountSchemes[];
67    private EditText mUsernameView;
68    private EditText mPasswordView;
69    private EditText mServerView;
70    private EditText mPortView;
71    private Spinner mSecurityTypeView;
72    private Spinner mDeletePolicyView;
73    private EditText mImapPathPrefixView;
74    private Button mNextButton;
75    private EmailContent.Account mAccount;
76    private boolean mMakeDefault;
77    private String mCacheLoginCredential;
78    private String mDuplicateAccountName;
79
80    public static void actionIncomingSettings(Activity fromActivity, EmailContent.Account account,
81            boolean makeDefault) {
82        Intent i = new Intent(fromActivity, AccountSetupIncoming.class);
83        i.putExtra(EXTRA_ACCOUNT, account);
84        i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
85        fromActivity.startActivity(i);
86    }
87
88    public static void actionEditIncomingSettings(Activity fromActivity, EmailContent.Account account)
89            {
90        Intent i = new Intent(fromActivity, AccountSetupIncoming.class);
91        i.setAction(Intent.ACTION_EDIT);
92        i.putExtra(EXTRA_ACCOUNT, account);
93        fromActivity.startActivity(i);
94    }
95
96    @Override
97    public void onCreate(Bundle savedInstanceState) {
98        super.onCreate(savedInstanceState);
99        setContentView(R.layout.account_setup_incoming);
100
101        mUsernameView = (EditText)findViewById(R.id.account_username);
102        mPasswordView = (EditText)findViewById(R.id.account_password);
103        TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
104        mServerView = (EditText)findViewById(R.id.account_server);
105        mPortView = (EditText)findViewById(R.id.account_port);
106        mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
107        mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy);
108        mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix);
109        mNextButton = (Button)findViewById(R.id.next);
110
111        mNextButton.setOnClickListener(this);
112
113        SpinnerOption securityTypes[] = {
114            new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
115            new SpinnerOption(1, getString(R.string.account_setup_incoming_security_ssl_label)),
116            new SpinnerOption(2, getString(
117                    R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
118            new SpinnerOption(3, getString(R.string.account_setup_incoming_security_tls_label)),
119            new SpinnerOption(4, getString(
120                    R.string.account_setup_incoming_security_tls_trust_certificates_label)),
121        };
122
123        SpinnerOption deletePolicies[] = {
124                new SpinnerOption(Account.DELETE_POLICY_NEVER,
125                        getString(R.string.account_setup_incoming_delete_policy_never_label)),
126                new SpinnerOption(Account.DELETE_POLICY_ON_DELETE,
127                        getString(R.string.account_setup_incoming_delete_policy_delete_label)),
128        };
129
130        ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this,
131                android.R.layout.simple_spinner_item, securityTypes);
132        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
133        mSecurityTypeView.setAdapter(securityTypesAdapter);
134
135        ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(this,
136                android.R.layout.simple_spinner_item, deletePolicies);
137        deletePoliciesAdapter
138                .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
139        mDeletePolicyView.setAdapter(deletePoliciesAdapter);
140
141        /*
142         * Updates the port when the user changes the security type. This allows
143         * us to show a reasonable default which the user can change.
144         */
145        mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
146            public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) {
147                updatePortFromSecurityType();
148            }
149
150            public void onNothingSelected(AdapterView<?> arg0) {
151            }
152        });
153
154        /*
155         * Calls validateFields() which enables or disables the Next button
156         * based on the fields' validity.
157         */
158        TextWatcher validationTextWatcher = new TextWatcher() {
159            public void afterTextChanged(Editable s) {
160                validateFields();
161            }
162
163            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
164            }
165
166            public void onTextChanged(CharSequence s, int start, int before, int count) {
167            }
168        };
169        mUsernameView.addTextChangedListener(validationTextWatcher);
170        mPasswordView.addTextChangedListener(validationTextWatcher);
171        mServerView.addTextChangedListener(validationTextWatcher);
172        mPortView.addTextChangedListener(validationTextWatcher);
173
174        /*
175         * Only allow digits in the port field.
176         */
177        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
178
179        mAccount = (EmailContent.Account)getIntent().getParcelableExtra(EXTRA_ACCOUNT);
180        mMakeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
181
182        /*
183         * If we're being reloaded we override the original account with the one
184         * we saved
185         */
186        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
187            mAccount = (EmailContent.Account)savedInstanceState.getParcelable(EXTRA_ACCOUNT);
188        }
189
190        try {
191            // TODO this should be accessed directly via the HostAuth structure
192            URI uri = new URI(mAccount.getStoreUri(this));
193            String username = null;
194            String password = null;
195            if (uri.getUserInfo() != null) {
196                String[] userInfoParts = uri.getUserInfo().split(":", 2);
197                username = userInfoParts[0];
198                if (userInfoParts.length > 1) {
199                    password = userInfoParts[1];
200                }
201            }
202
203            if (username != null) {
204                mUsernameView.setText(username);
205            }
206
207            if (password != null) {
208                mPasswordView.setText(password);
209            }
210
211            if (uri.getScheme().startsWith("pop3")) {
212                serverLabelView.setText(R.string.account_setup_incoming_pop_server_label);
213                mAccountPorts = POP_PORTS;
214                mAccountSchemes = POP_SCHEMES;
215
216                findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
217            } else if (uri.getScheme().startsWith("imap")) {
218                serverLabelView.setText(R.string.account_setup_incoming_imap_server_label);
219                mAccountPorts = IMAP_PORTS;
220                mAccountSchemes = IMAP_SCHEMES;
221
222                findViewById(R.id.account_delete_policy_label).setVisibility(View.GONE);
223                mDeletePolicyView.setVisibility(View.GONE);
224                if (uri.getPath() != null && uri.getPath().length() > 0) {
225                    mImapPathPrefixView.setText(uri.getPath().substring(1));
226                }
227            } else {
228                throw new Error("Unknown account type: " + mAccount.getStoreUri(this));
229            }
230
231            for (int i = 0; i < mAccountSchemes.length; i++) {
232                if (mAccountSchemes[i].equals(uri.getScheme())) {
233                    SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
234                }
235            }
236
237            SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mAccount.getDeletePolicy());
238
239            if (uri.getHost() != null) {
240                mServerView.setText(uri.getHost());
241            }
242
243            if (uri.getPort() != -1) {
244                mPortView.setText(Integer.toString(uri.getPort()));
245            } else {
246                updatePortFromSecurityType();
247            }
248        } catch (URISyntaxException use) {
249            /*
250             * We should always be able to parse our own settings.
251             */
252            throw new Error(use);
253        }
254
255        validateFields();
256    }
257
258    @Override
259    public void onSaveInstanceState(Bundle outState) {
260        super.onSaveInstanceState(outState);
261        outState.putParcelable(EXTRA_ACCOUNT, mAccount);
262    }
263
264    /**
265     * Prepare a cached dialog with current values (e.g. account name)
266     */
267    @Override
268    public Dialog onCreateDialog(int id) {
269        switch (id) {
270            case DIALOG_DUPLICATE_ACCOUNT:
271                return new AlertDialog.Builder(this)
272                    .setIcon(android.R.drawable.ic_dialog_alert)
273                    .setTitle(R.string.account_duplicate_dlg_title)
274                    .setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
275                            mDuplicateAccountName))
276                    .setPositiveButton(R.string.okay_action,
277                            new DialogInterface.OnClickListener() {
278                        public void onClick(DialogInterface dialog, int which) {
279                            dismissDialog(DIALOG_DUPLICATE_ACCOUNT);
280                        }
281                    })
282                    .create();
283        }
284        return null;
285    }
286
287    /**
288     * Update a cached dialog with current values (e.g. account name)
289     */
290    @Override
291    public void onPrepareDialog(int id, Dialog dialog) {
292        switch (id) {
293            case DIALOG_DUPLICATE_ACCOUNT:
294                if (mDuplicateAccountName != null) {
295                    AlertDialog alert = (AlertDialog) dialog;
296                    alert.setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
297                            mDuplicateAccountName));
298                }
299                break;
300        }
301    }
302
303    /**
304     * Check the values in the fields and decide if it makes sense to enable the "next" button
305     * NOTE:  Does it make sense to extract & combine with similar code in AccountSetupIncoming?
306     */
307    private void validateFields() {
308        boolean enabled = Utility.requiredFieldValid(mUsernameView)
309                && Utility.requiredFieldValid(mPasswordView)
310                && Utility.requiredFieldValid(mServerView)
311                && Utility.requiredFieldValid(mPortView);
312        if (enabled) {
313            try {
314                URI uri = getUri();
315            } catch (URISyntaxException use) {
316                enabled = false;
317            }
318        }
319        mNextButton.setEnabled(enabled);
320        Utility.setCompoundDrawablesAlpha(mNextButton, enabled ? 255 : 128);
321    }
322
323    private void updatePortFromSecurityType() {
324        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
325        mPortView.setText(Integer.toString(mAccountPorts[securityType]));
326    }
327
328    @Override
329    public void onActivityResult(int requestCode, int resultCode, Intent data) {
330        if (resultCode == RESULT_OK) {
331            if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
332                if (mAccount.isSaved()) {
333                    mAccount.update(this, mAccount.toContentValues());
334                    mAccount.mHostAuthRecv.update(this, mAccount.mHostAuthRecv.toContentValues());
335                } else {
336                    mAccount.save(this);
337                }
338                // Update the backup (side copy) of the accounts
339                AccountBackupRestore.backupAccounts(this);
340                finish();
341            } else {
342                /*
343                 * Set the username and password for the outgoing settings to the username and
344                 * password the user just set for incoming.
345                 */
346                try {
347                    URI oldUri = new URI(mAccount.getSenderUri(this));
348                    URI uri = new URI(
349                            oldUri.getScheme(),
350                            mUsernameView.getText().toString().trim() + ":"
351                                    + mPasswordView.getText().toString().trim(),
352                            oldUri.getHost(),
353                            oldUri.getPort(),
354                            null,
355                            null,
356                            null);
357                    mAccount.setSenderUri(this, uri.toString());
358                } catch (URISyntaxException use) {
359                    /*
360                     * If we can't set up the URL we just continue. It's only for
361                     * convenience.
362                     */
363                }
364
365                AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault);
366                finish();
367            }
368        }
369    }
370
371    /**
372     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
373     * a problem with the user input.
374     * @return a URI built from the account setup fields
375     */
376    private URI getUri() throws URISyntaxException {
377        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
378        String path = null;
379        if (mAccountSchemes[securityType].startsWith("imap")) {
380            path = "/" + mImapPathPrefixView.getText().toString().trim();
381        }
382        String userName = mUsernameView.getText().toString().trim();
383        mCacheLoginCredential = userName;
384        URI uri = new URI(
385                mAccountSchemes[securityType],
386                userName + ":" + mPasswordView.getText().toString().trim(),
387                mServerView.getText().toString().trim(),
388                Integer.parseInt(mPortView.getText().toString().trim()),
389                path, // path
390                null, // query
391                null);
392
393        return uri;
394    }
395
396    private void onNext() {
397        try {
398            URI uri = getUri();
399            mAccount.setStoreUri(this, uri.toString());
400
401            // Stop here if the login credentials duplicate an existing account
402            // (unless they duplicate the existing account, as they of course will)
403            EmailContent.Account account = Utility.findExistingAccount(this, mAccount.mId,
404                    uri.getHost(), mCacheLoginCredential);
405            if (account != null) {
406                mDuplicateAccountName = account.mDisplayName;
407                this.showDialog(DIALOG_DUPLICATE_ACCOUNT);
408                return;
409            }
410        } catch (URISyntaxException use) {
411            /*
412             * It's unrecoverable if we cannot create a URI from components that
413             * we validated to be safe.
414             */
415            throw new Error(use);
416        }
417
418        mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
419        AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false);
420    }
421
422    public void onClick(View v) {
423        switch (v.getId()) {
424            case R.id.next:
425                onNext();
426                break;
427        }
428    }
429}
430