michael@0: /**
michael@0: * Copyright (c) 2012-2013, Gerald Garcia
michael@8: *
michael@0: * This file is part of Andoid Caldav Sync Adapter Free.
michael@0: *
michael@0: * Andoid Caldav Sync Adapter Free is free software: you can redistribute
michael@0: * it and/or modify it under the terms of the GNU General Public License
michael@0: * as published by the Free Software Foundation, either version 3 of the
michael@0: * License, or at your option any later version.
michael@0: *
michael@0: * Andoid Caldav Sync Adapter Free is distributed in the hope that
michael@0: * it will be useful, but WITHOUT ANY WARRANTY; without even the implied
michael@0: * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
michael@0: * GNU General Public License for more details.
michael@0: *
michael@0: * You should have received a copy of the GNU General Public License
michael@0: * along with Andoid Caldav Sync Adapter Free.
michael@0: * If not, see .
michael@8: *
michael@0: */
michael@0:
michael@0: package org.gege.caldavsyncadapter.authenticator;
michael@0:
michael@0: import android.accounts.Account;
michael@0: import android.accounts.AccountManager;
michael@0: import android.animation.Animator;
michael@0: import android.animation.AnimatorListenerAdapter;
michael@0: import android.annotation.TargetApi;
michael@0: import android.app.Activity;
michael@0: import android.content.Context;
michael@0: import android.content.pm.PackageManager.NameNotFoundException;
michael@0: import android.os.AsyncTask;
michael@0: import android.os.Build;
michael@0: import android.os.Bundle;
michael@8: import android.text.Editable;
michael@0: import android.text.TextUtils;
michael@8: import android.text.TextWatcher;
michael@0: import android.util.Log;
michael@0: import android.view.KeyEvent;
michael@0: import android.view.Menu;
michael@0: import android.view.View;
michael@0: import android.view.inputmethod.EditorInfo;
michael@8: import android.widget.CheckBox;
michael@0: import android.widget.EditText;
michael@0: import android.widget.TextView;
michael@0: import android.widget.Toast;
michael@0:
michael@8: import org.apache.http.conn.HttpHostConnectException;
michael@8: import org.gege.caldavsyncadapter.Constants;
michael@8: import org.gege.caldavsyncadapter.R;
michael@8: import org.gege.caldavsyncadapter.caldav.CaldavFacade;
michael@8: import org.gege.caldavsyncadapter.caldav.CaldavFacade.TestConnectionResult;
michael@8: import org.xml.sax.SAXException;
michael@8:
michael@8: import java.io.IOException;
michael@8: import java.io.UnsupportedEncodingException;
michael@8: import java.net.MalformedURLException;
michael@8: import java.net.URISyntaxException;
michael@8: import java.util.Locale;
michael@8:
michael@8: import javax.xml.parsers.ParserConfigurationException;
michael@8:
michael@0: /**
michael@0: * Activity which displays a login screen to the user, offering registration as
michael@0: * well.
michael@0: */
michael@0: public class AuthenticatorActivity extends Activity {
michael@0:
michael@8: private static final String TAG = "AuthenticatorActivity";
michael@0:
michael@8: private static final String ACCOUNT_TYPE = "org.gege.caldavsyncadapter.account";
michael@0:
michael@8: public static final String USER_DATA_URL_KEY = "USER_DATA_URL_KEY";
michael@8: public static final String USER_DATA_USERNAME = "USER_DATA_USERNAME";
michael@8: public static final String USER_DATA_VERSION = "USER_DATA_VERSION";
michael@8: public static final String CURRENT_USER_DATA_VERSION = "1";
michael@0:
michael@8: public static final String ACCOUNT_NAME_SPLITTER = "@";
michael@0:
michael@8: /**
michael@8: * The default email to populate the email field with.
michael@8: */
michael@8: public static final String EXTRA_EMAIL = "com.example.android.authenticatordemo.extra.EMAIL";
michael@0:
michael@8: /**
michael@8: * Keep track of the login task to ensure we can cancel it if requested.
michael@8: */
michael@8: private UserLoginTask mAuthTask = null;
michael@0:
michael@8: // Values for email and password at the time of the login attempt.
michael@8: private String mUser;
michael@8: private String mPassword;
michael@8: private String mTrustAll;
michael@8: private Context mContext;
michael@0:
michael@8: // UI references.
michael@8: private EditText mUserView;
michael@8: private EditText mPasswordView;
michael@8: private View mLoginFormView;
michael@8: private View mLoginStatusView;
michael@8: private TextView mLoginStatusMessageView;
michael@8: private CheckBox mTrustCheckBox;
michael@0:
michael@8: private AccountManager mAccountManager;
michael@0:
michael@8: private String mURL;
michael@8: private EditText mURLView;
michael@0:
michael@8: private String mAccountname;
michael@8: private EditText mAccountnameView;
michael@0:
michael@8: public AuthenticatorActivity() {
michael@8: super();
michael@0:
michael@8: }
michael@0:
michael@8: @Override
michael@8: protected void onCreate(Bundle savedInstanceState) {
michael@8: super.onCreate(savedInstanceState);
michael@0:
michael@8: mAccountManager = AccountManager.get(this);
michael@0:
michael@8: setContentView(R.layout.activity_authenticator);
michael@0:
michael@8: // Set up the login form.
michael@8: mUser = getIntent().getStringExtra(EXTRA_EMAIL);
michael@8: mUserView = (EditText) findViewById(R.id.user);
michael@8: mUserView.setText(mUser);
michael@0:
michael@8: mContext = getBaseContext();
michael@0:
michael@8: mPasswordView = (EditText) findViewById(R.id.password);
michael@8: mPasswordView
michael@8: .setOnEditorActionListener(new TextView.OnEditorActionListener() {
michael@8: @Override
michael@8: public boolean onEditorAction(TextView textView, int id,
michael@8: KeyEvent keyEvent) {
michael@8: if (id == R.id.login || id == EditorInfo.IME_NULL) {
michael@8: attemptLogin();
michael@8: return true;
michael@8: }
michael@8: return false;
michael@8: }
michael@8: });
michael@0:
michael@0:
michael@8: mURLView = (EditText) findViewById(R.id.url);
michael@8: // if the URL start with "https" show the option to disable SSL host verification
michael@8: mURLView.addTextChangedListener(new TextWatcher() {
michael@8: @Override
michael@8: public void onTextChanged(CharSequence s, int start, int before, int count) {
michael@8: String url = ((EditText) findViewById(R.id.url)).getText().toString();
michael@8: int visible = url.toLowerCase(Locale.getDefault())
michael@8: .startsWith("https") ? View.VISIBLE : View.GONE;
michael@8: ((CheckBox) findViewById(R.id.trustall)).setVisibility(visible);
michael@8: }
michael@0:
michael@8: @Override
michael@8: public void beforeTextChanged(CharSequence s, int start, int count,
michael@8: int after) {
michael@8: }
michael@0:
michael@8: @Override
michael@8: public void afterTextChanged(Editable s) {
michael@8: }
michael@8: });
michael@0:
michael@8: mAccountnameView = (EditText) findViewById(R.id.accountname);
michael@0:
michael@8: mLoginFormView = findViewById(R.id.login_form);
michael@8: mLoginStatusView = findViewById(R.id.login_status);
michael@8: mLoginStatusMessageView = (TextView) findViewById(R.id.login_status_message);
michael@0:
michael@8: findViewById(R.id.sign_in_button).setOnClickListener(
michael@8: new View.OnClickListener() {
michael@8: @Override
michael@8: public void onClick(View view) {
michael@8: attemptLogin();
michael@8: }
michael@8: }
michael@8: );
michael@0:
michael@8: mTrustCheckBox = (CheckBox) findViewById(R.id.trustall);
michael@0:
michael@0:
michael@8: }
michael@0:
michael@8: @Override
michael@8: public boolean onCreateOptionsMenu(Menu menu) {
michael@8: super.onCreateOptionsMenu(menu);
michael@8: return true;
michael@8: }
michael@0:
michael@8: /**
michael@8: * Attempts to sign in or register the account specified by the login form.
michael@8: * If there are form errors (invalid email, missing fields, etc.), the
michael@8: * errors are presented and no actual login attempt is made.
michael@8: */
michael@8: public void attemptLogin() {
michael@8: if (mAuthTask != null) {
michael@8: return;
michael@8: }
michael@0:
michael@8: // Reset errors.
michael@8: mUserView.setError(null);
michael@8: mPasswordView.setError(null);
michael@8:
michael@8: // Store values at the time of the login attempt.
michael@8: mUser = mUserView.getText().toString();
michael@8: mPassword = mPasswordView.getText().toString();
michael@8: mURL = mURLView.getText().toString();
michael@8: mAccountname = mAccountnameView.getText().toString();
michael@8: mTrustAll = (mTrustCheckBox.isChecked() ? "false" : "true");
michael@8: boolean cancel = false;
michael@8: View focusView = null;
michael@8:
michael@8: if (!mAccountname.equals("")) {
michael@8: Account TestAccount = new Account(mAccountname, ACCOUNT_TYPE);
michael@8: String TestUrl = mAccountManager.getUserData(TestAccount, AuthenticatorActivity.USER_DATA_URL_KEY);
michael@8: if (TestUrl != null) {
michael@8: mAccountnameView.setError(getString(R.string.error_account_already_in_use));
michael@8: focusView = mAccountnameView;
michael@8: cancel = true;
michael@8: }
michael@8: }
michael@8:
michael@8: // Check for a valid password.
michael@8: if (TextUtils.isEmpty(mPassword)) {
michael@8: mPasswordView.setError(getString(R.string.error_field_required));
michael@8: focusView = mPasswordView;
michael@8: cancel = true;
michael@8: }
michael@8:
michael@8: // Check for a valid email address.
michael@8: if (TextUtils.isEmpty(mUser)) {
michael@8: mUserView.setError(getString(R.string.error_field_required));
michael@8: focusView = mUserView;
michael@8: cancel = true;
michael@8: }
michael@8: //else if (!mUser.contains("@")) {
michael@8: // mUserView.setError(getString(R.string.error_invalid_email));
michael@8: // focusView = mUserView;
michael@8: // cancel = true;
michael@8: //}
michael@8:
michael@8: if (cancel) {
michael@8: // There was an error; don't attempt login and focus the first
michael@8: // form field with an error.
michael@8: focusView.requestFocus();
michael@8: } else {
michael@8: // Show a progress spinner, and kick off a background task to
michael@8: // perform the user login attempt.
michael@8: mLoginStatusMessageView.setText(R.string.login_progress_signing_in);
michael@8: showProgress(true);
michael@8: mAuthTask = new UserLoginTask();
michael@8: mAuthTask.execute((Void) null);
michael@8: }
michael@8: }
michael@8:
michael@8: /**
michael@8: * Shows the progress UI and hides the login form.
michael@8: */
michael@8: @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
michael@8: private void showProgress(final boolean show) {
michael@8: // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
michael@8: // for very easy animations. If available, use these APIs to fade-in
michael@8: // the progress spinner.
michael@8: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
michael@8: int shortAnimTime = getResources().getInteger(
michael@8: android.R.integer.config_shortAnimTime);
michael@8:
michael@8: mLoginStatusView.setVisibility(View.VISIBLE);
michael@8: mLoginStatusView.animate().setDuration(shortAnimTime)
michael@8: .alpha(show ? 1 : 0)
michael@8: .setListener(new AnimatorListenerAdapter() {
michael@8: @Override
michael@8: public void onAnimationEnd(Animator animation) {
michael@8: mLoginStatusView.setVisibility(show ? View.VISIBLE
michael@8: : View.GONE);
michael@8: }
michael@8: });
michael@8:
michael@8: mLoginFormView.setVisibility(View.VISIBLE);
michael@8: mLoginFormView.animate().setDuration(shortAnimTime)
michael@8: .alpha(show ? 0 : 1)
michael@8: .setListener(new AnimatorListenerAdapter() {
michael@8: @Override
michael@8: public void onAnimationEnd(Animator animation) {
michael@8: mLoginFormView.setVisibility(show ? View.GONE
michael@8: : View.VISIBLE);
michael@8: }
michael@8: });
michael@8: } else {
michael@8: // The ViewPropertyAnimator APIs are not available, so simply show
michael@8: // and hide the relevant UI components.
michael@8: mLoginStatusView.setVisibility(show ? View.VISIBLE : View.GONE);
michael@8: mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
michael@8: }
michael@8: }
michael@8:
michael@8:
michael@8: protected enum LoginResult {
michael@8: MalformedURLException,
michael@8: GeneralSecurityException,
michael@8: UnkonwnException,
michael@8: WrongCredentials,
michael@8: InvalidResponse,
michael@8: WrongUrl,
michael@8: ConnectionRefused,
michael@8: Success_Calendar,
michael@8: Success_Collection,
michael@8: UNTRUSTED_CERT,
michael@8: Account_Already_In_Use
michael@8: }
michael@8:
michael@8:
michael@8: /**
michael@8: * Represents an asynchronous login/registration task used to authenticate
michael@8: * the user.
michael@8: */
michael@8: public class UserLoginTask extends AsyncTask {
michael@8:
michael@8: @Override
michael@8: protected LoginResult doInBackground(Void... params) {
michael@8:
michael@8: TestConnectionResult result = null;
michael@8:
michael@8: try {
michael@8: CaldavFacade facade = new CaldavFacade(mUser, mPassword, mURL, mTrustAll);
michael@8: String version = "";
michael@8: try {
michael@8: version = mContext.getPackageManager()
michael@8: .getPackageInfo(mContext.getPackageName(), 0).versionName;
michael@8: } catch (NameNotFoundException e) {
michael@8: version = "unknown";
michael@8: e.printStackTrace();
michael@8: }
michael@8: facade.setVersion(version);
michael@8: result = facade.testConnection();
michael@8: Log.i(TAG, "testConnection status=" + result);
michael@8: } catch (HttpHostConnectException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.ConnectionRefused;
michael@8: } catch (MalformedURLException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.MalformedURLException;
michael@8: } catch (UnsupportedEncodingException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.UnkonwnException;
michael@8: } catch (ParserConfigurationException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.UnkonwnException;
michael@8: } catch (SAXException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.InvalidResponse;
michael@8: } catch (IOException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.UnkonwnException;
michael@8: } catch (URISyntaxException e) {
michael@8: Log.w(TAG, "testConnection", e);
michael@8: return LoginResult.MalformedURLException;
michael@8: }
michael@8:
michael@8: if (result == null) {
michael@8: return LoginResult.UnkonwnException;
michael@8: }
michael@8:
michael@8: switch (result) {
michael@8:
michael@8: case SSL_ERROR:
michael@8: return LoginResult.UNTRUSTED_CERT;
michael@8: case SUCCESS:
michael@8: boolean OldAccount = false;
michael@8: LoginResult Result = LoginResult.Success_Calendar;
michael@8:
michael@8: if (OldAccount) {
michael@8: final Account account = new Account(mUser, ACCOUNT_TYPE);
michael@8: if (mAccountManager.addAccountExplicitly(account, mPassword, null)) {
michael@8: Log.v(TAG, "new account created");
michael@8: mAccountManager.setUserData(account, USER_DATA_URL_KEY, mURL);
michael@8: } else {
michael@8: Log.v(TAG, "no new account created");
michael@8: Result = LoginResult.Account_Already_In_Use;
michael@8: }
michael@8: } else {
michael@8: final Account account;
michael@8: if (mAccountname.equals("")) {
michael@8: account = new Account(mUser + ACCOUNT_NAME_SPLITTER + mURL, ACCOUNT_TYPE);
michael@8: } else {
michael@8: account = new Account(mAccountname, ACCOUNT_TYPE);
michael@8: }
michael@8: if (mAccountManager.addAccountExplicitly(account, mPassword, null)) {
michael@8: Log.v(TAG, "new account created");
michael@8: mAccountManager.setUserData(account, USER_DATA_URL_KEY, mURL);
michael@8: mAccountManager.setUserData(account, USER_DATA_USERNAME, mUser);
michael@8: mAccountManager.setUserData(account, USER_DATA_VERSION, CURRENT_USER_DATA_VERSION);
michael@8: mAccountManager.setUserData(account, Constants.USER_DATA_TRUST_ALL_KEY, mTrustAll);
michael@8: } else {
michael@8: Log.v(TAG, "no new account created");
michael@8: Result = LoginResult.Account_Already_In_Use;
michael@8: }
michael@8: }
michael@8:
michael@8: return Result;
michael@8:
michael@8: case WRONG_CREDENTIAL:
michael@8: return LoginResult.WrongCredentials;
michael@8:
michael@8: case WRONG_SERVER_STATUS:
michael@8: return LoginResult.InvalidResponse;
michael@8:
michael@8: case WRONG_URL:
michael@8: return LoginResult.WrongUrl;
michael@8:
michael@8: case WRONG_ANSWER:
michael@8: return LoginResult.InvalidResponse;
michael@8:
michael@8: default:
michael@8: return LoginResult.UnkonwnException;
michael@8:
michael@8: }
michael@8:
michael@8: }
michael@8:
michael@8:
michael@8: @Override
michael@8: protected void onPostExecute(final LoginResult result) {
michael@8: mAuthTask = null;
michael@8: showProgress(false);
michael@8:
michael@8: int duration = Toast.LENGTH_SHORT;
michael@8: Toast toast = null;
michael@8:
michael@8: switch (result) {
michael@8: case Success_Calendar:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.success_calendar, duration);
michael@8: toast.show();
michael@8: finish();
michael@8: break;
michael@8:
michael@8: case Success_Collection:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.success_collection, duration);
michael@8: toast.show();
michael@8: finish();
michael@8: break;
michael@8:
michael@8: case MalformedURLException:
michael@8:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_incorrect_url_format, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_incorrect_url_format));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: case InvalidResponse:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_invalid_server_answer, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_invalid_server_answer));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: case WrongUrl:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_wrong_url, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_wrong_url));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8:
michael@8: case GeneralSecurityException:
michael@8: break;
michael@8: case UnkonwnException:
michael@8: break;
michael@8: case WrongCredentials:
michael@8: mPasswordView.setError(getString(R.string.error_incorrect_password));
michael@8: mPasswordView.requestFocus();
michael@8: break;
michael@8:
michael@8: case ConnectionRefused:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_connection_refused, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_connection_refused));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: case UNTRUSTED_CERT:
michael@8: toast = Toast.makeText(getApplicationContext(), getString(R.string.error_untrusted_certificate), duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_ssl));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: case Account_Already_In_Use:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_account_already_in_use, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_account_already_in_use));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: default:
michael@8: toast = Toast.makeText(getApplicationContext(), R.string.error_unkown_error, duration);
michael@8: toast.show();
michael@8: mURLView.setError(getString(R.string.error_unkown_error));
michael@8: mURLView.requestFocus();
michael@8: break;
michael@8: }
michael@8:
michael@8:
michael@8: }
michael@8:
michael@8: @Override
michael@8: protected void onCancelled() {
michael@8: mAuthTask = null;
michael@8: showProgress(false);
michael@8: }
michael@8: }
michael@8: }