AccountSetupIncoming.java revision da8836a76cd8a6eaa7e3693eeacc6393870b2066
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.R;
20import com.android.email.Utility;
21import com.android.email.provider.EmailContent;
22
23import android.app.Activity;
24import android.app.AlertDialog;
25import android.app.Dialog;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.os.Bundle;
29import android.text.Editable;
30import android.text.TextWatcher;
31import android.text.method.DigitsKeyListener;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.widget.AdapterView;
35import android.widget.ArrayAdapter;
36import android.widget.Button;
37import android.widget.EditText;
38import android.widget.Spinner;
39import android.widget.TextView;
40
41import java.net.URI;
42import java.net.URISyntaxException;
43
44public class AccountSetupIncoming extends Activity implements OnClickListener {
45    private static final String EXTRA_ACCOUNT = "account";
46    private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
47
48    private static final int popPorts[] = {
49            110, 995, 995, 110, 110
50    };
51    private static final String popSchemes[] = {
52            "pop3", "pop3+ssl", "pop3+ssl+", "pop3+tls", "pop3+tls+"
53    };
54    private static final int imapPorts[] = {
55            143, 993, 993, 143, 143
56    };
57    private static final String imapSchemes[] = {
58            "imap", "imap+ssl", "imap+ssl+", "imap+tls", "imap+tls+"
59    };
60
61    private final static int DIALOG_DUPLICATE_ACCOUNT = 1;
62
63    private int mAccountPorts[];
64    private String mAccountSchemes[];
65    private EditText mUsernameView;
66    private EditText mPasswordView;
67    private EditText mServerView;
68    private EditText mPortView;
69    private Spinner mSecurityTypeView;
70    private Spinner mDeletePolicyView;
71    private EditText mImapPathPrefixView;
72    private Button mNextButton;
73    private EmailContent.Account mAccount;
74    private boolean mMakeDefault;
75    private String mCacheLoginCredential;
76    private String mDuplicateAccountName;
77
78    public static void actionIncomingSettings(Activity fromActivity, EmailContent.Account account,
79            boolean makeDefault) {
80        Intent i = new Intent(fromActivity, AccountSetupIncoming.class);
81        i.putExtra(EXTRA_ACCOUNT, account);
82        i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
83        fromActivity.startActivity(i);
84    }
85
86    public static void actionEditIncomingSettings(Activity fromActivity, EmailContent.Account account)
87            {
88        Intent i = new Intent(fromActivity, AccountSetupIncoming.class);
89        i.setAction(Intent.ACTION_EDIT);
90        i.putExtra(EXTRA_ACCOUNT, account);
91        fromActivity.startActivity(i);
92    }
93
94    @Override
95    public void onCreate(Bundle savedInstanceState) {
96        super.onCreate(savedInstanceState);
97        setContentView(R.layout.account_setup_incoming);
98
99        mUsernameView = (EditText)findViewById(R.id.account_username);
100        mPasswordView = (EditText)findViewById(R.id.account_password);
101        TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
102        mServerView = (EditText)findViewById(R.id.account_server);
103        mPortView = (EditText)findViewById(R.id.account_port);
104        mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
105        mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy);
106        mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix);
107        mNextButton = (Button)findViewById(R.id.next);
108
109        mNextButton.setOnClickListener(this);
110
111        SpinnerOption securityTypes[] = {
112                new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
113                new SpinnerOption(1,
114                        getString(R.string.account_setup_incoming_security_ssl_optional_label)),
115                new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)),
116                new SpinnerOption(3,
117                        getString(R.string.account_setup_incoming_security_tls_optional_label)),
118                new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
119        };
120
121        SpinnerOption deletePolicies[] = {
122                new SpinnerOption(0,
123                        getString(R.string.account_setup_incoming_delete_policy_never_label)),
124                new SpinnerOption(1,
125                        getString(R.string.account_setup_incoming_delete_policy_7days_label)),
126                new SpinnerOption(2,
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 = popPorts;
214                mAccountSchemes = popSchemes;
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 = imapPorts;
220                mAccountSchemes = imapSchemes;
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                // TODO Review carefully to make sure this is bulletproof
333                if (mAccount.isSaved()) {
334                    mAccount.update(this, mAccount.toContentValues());
335                } else {
336                    mAccount.save(this);
337                }
338                finish();
339            } else {
340                /*
341                 * Set the username and password for the outgoing settings to the username and
342                 * password the user just set for incoming.
343                 */
344                try {
345                    URI oldUri = new URI(mAccount.getSenderUri(this));
346                    URI uri = new URI(
347                            oldUri.getScheme(),
348                            mUsernameView.getText().toString().trim() + ":"
349                                    + mPasswordView.getText().toString().trim(),
350                            oldUri.getHost(),
351                            oldUri.getPort(),
352                            null,
353                            null,
354                            null);
355                    mAccount.setSenderUri(this, uri.toString());
356                } catch (URISyntaxException use) {
357                    /*
358                     * If we can't set up the URL we just continue. It's only for
359                     * convenience.
360                     */
361                }
362
363                AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault);
364                finish();
365            }
366        }
367    }
368
369    /**
370     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
371     * a problem with the user input.
372     * @return a URI built from the account setup fields
373     */
374    private URI getUri() throws URISyntaxException {
375        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
376        String path = null;
377        if (mAccountSchemes[securityType].startsWith("imap")) {
378            path = "/" + mImapPathPrefixView.getText().toString().trim();
379        }
380        String userName = mUsernameView.getText().toString().trim();
381        mCacheLoginCredential = userName;
382        URI uri = new URI(
383                mAccountSchemes[securityType],
384                userName + ":" + mPasswordView.getText().toString().trim(),
385                mServerView.getText().toString().trim(),
386                Integer.parseInt(mPortView.getText().toString().trim()),
387                path, // path
388                null, // query
389                null);
390
391        return uri;
392    }
393
394    private void onNext() {
395        try {
396            URI uri = getUri();
397            mAccount.setStoreUri(this, uri.toString());
398
399            // Stop here if the login credentials duplicate an existing account
400            // (unless they duplicate the existing account, as they of course will)
401            mDuplicateAccountName = Utility.findDuplicateAccount(this, mAccount.mId,
402                    uri.getHost(), mCacheLoginCredential);
403            if (mDuplicateAccountName != null) {
404                this.showDialog(DIALOG_DUPLICATE_ACCOUNT);
405                return;
406            }
407        } catch (URISyntaxException use) {
408            /*
409             * It's unrecoverable if we cannot create a URI from components that
410             * we validated to be safe.
411             */
412            throw new Error(use);
413        }
414
415        mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
416        AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false);
417    }
418
419    public void onClick(View v) {
420        switch (v.getId()) {
421            case R.id.next:
422                onNext();
423                break;
424        }
425    }
426}
427