1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.example.android.samplesync.authenticator;
18
19import com.example.android.samplesync.Constants;
20import com.example.android.samplesync.R;
21import com.example.android.samplesync.client.NetworkUtilities;
22
23import android.accounts.Account;
24import android.accounts.AccountAuthenticatorActivity;
25import android.accounts.AccountManager;
26import android.app.Dialog;
27import android.app.ProgressDialog;
28import android.content.ContentResolver;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.Handler;
34import android.provider.ContactsContract;
35import android.text.TextUtils;
36import android.util.Log;
37import android.view.View;
38import android.view.Window;
39import android.widget.EditText;
40import android.widget.TextView;
41
42/**
43 * Activity which displays login screen to the user.
44 */
45public class AuthenticatorActivity extends AccountAuthenticatorActivity {
46    /** The Intent flag to confirm credentials. */
47    public static final String PARAM_CONFIRM_CREDENTIALS = "confirmCredentials";
48
49    /** The Intent extra to store password. */
50    public static final String PARAM_PASSWORD = "password";
51
52    /** The Intent extra to store username. */
53    public static final String PARAM_USERNAME = "username";
54
55    /** The Intent extra to store username. */
56    public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType";
57
58    /** The tag used to log to adb console. */
59    private static final String TAG = "AuthenticatorActivity";
60    private AccountManager mAccountManager;
61
62    /** Keep track of the login task so can cancel it if requested */
63    private UserLoginTask mAuthTask = null;
64
65    /** Keep track of the progress dialog so we can dismiss it */
66    private ProgressDialog mProgressDialog = null;
67
68    /**
69     * If set we are just checking that the user knows their credentials; this
70     * doesn't cause the user's password or authToken to be changed on the
71     * device.
72     */
73    private Boolean mConfirmCredentials = false;
74
75    /** for posting authentication attempts back to UI thread */
76    private final Handler mHandler = new Handler();
77
78    private TextView mMessage;
79
80    private String mPassword;
81
82    private EditText mPasswordEdit;
83
84    /** Was the original caller asking for an entirely new account? */
85    protected boolean mRequestNewAccount = false;
86
87    private String mUsername;
88
89    private EditText mUsernameEdit;
90
91    /**
92     * {@inheritDoc}
93     */
94    @Override
95    public void onCreate(Bundle icicle) {
96
97        Log.i(TAG, "onCreate(" + icicle + ")");
98        super.onCreate(icicle);
99        mAccountManager = AccountManager.get(this);
100        Log.i(TAG, "loading data from Intent");
101        final Intent intent = getIntent();
102        mUsername = intent.getStringExtra(PARAM_USERNAME);
103        mRequestNewAccount = mUsername == null;
104        mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
105        Log.i(TAG, "    request new: " + mRequestNewAccount);
106        requestWindowFeature(Window.FEATURE_LEFT_ICON);
107        setContentView(R.layout.login_activity);
108        getWindow().setFeatureDrawableResource(
109                Window.FEATURE_LEFT_ICON, android.R.drawable.ic_dialog_alert);
110        mMessage = (TextView) findViewById(R.id.message);
111        mUsernameEdit = (EditText) findViewById(R.id.username_edit);
112        mPasswordEdit = (EditText) findViewById(R.id.password_edit);
113        if (!TextUtils.isEmpty(mUsername)) mUsernameEdit.setText(mUsername);
114        mMessage.setText(getMessage());
115    }
116
117    /*
118     * {@inheritDoc}
119     */
120    @Override
121    protected Dialog onCreateDialog(int id, Bundle args) {
122        final ProgressDialog dialog = new ProgressDialog(this);
123        dialog.setMessage(getText(R.string.ui_activity_authenticating));
124        dialog.setIndeterminate(true);
125        dialog.setCancelable(true);
126        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
127            public void onCancel(DialogInterface dialog) {
128                Log.i(TAG, "user cancelling authentication");
129                if (mAuthTask != null) {
130                    mAuthTask.cancel(true);
131                }
132            }
133        });
134        // We save off the progress dialog in a field so that we can dismiss
135        // it later. We can't just call dismissDialog(0) because the system
136        // can lose track of our dialog if there's an orientation change.
137        mProgressDialog = dialog;
138        return dialog;
139    }
140
141    /**
142     * Handles onClick event on the Submit button. Sends username/password to
143     * the server for authentication. The button is configured to call
144     * handleLogin() in the layout XML.
145     *
146     * @param view The Submit button for which this method is invoked
147     */
148    public void handleLogin(View view) {
149        if (mRequestNewAccount) {
150            mUsername = mUsernameEdit.getText().toString();
151        }
152        mPassword = mPasswordEdit.getText().toString();
153        if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {
154            mMessage.setText(getMessage());
155        } else {
156            // Show a progress dialog, and kick off a background task to perform
157            // the user login attempt.
158            showProgress();
159            mAuthTask = new UserLoginTask();
160            mAuthTask.execute();
161        }
162    }
163
164    /**
165     * Called when response is received from the server for confirm credentials
166     * request. See onAuthenticationResult(). Sets the
167     * AccountAuthenticatorResult which is sent back to the caller.
168     *
169     * @param result the confirmCredentials result.
170     */
171    private void finishConfirmCredentials(boolean result) {
172        Log.i(TAG, "finishConfirmCredentials()");
173        final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
174        mAccountManager.setPassword(account, mPassword);
175        final Intent intent = new Intent();
176        intent.putExtra(AccountManager.KEY_BOOLEAN_RESULT, result);
177        setAccountAuthenticatorResult(intent.getExtras());
178        setResult(RESULT_OK, intent);
179        finish();
180    }
181
182    /**
183     * Called when response is received from the server for authentication
184     * request. See onAuthenticationResult(). Sets the
185     * AccountAuthenticatorResult which is sent back to the caller. We store the
186     * authToken that's returned from the server as the 'password' for this
187     * account - so we're never storing the user's actual password locally.
188     *
189     * @param result the confirmCredentials result.
190     */
191    private void finishLogin(String authToken) {
192
193        Log.i(TAG, "finishLogin()");
194        final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
195        if (mRequestNewAccount) {
196            mAccountManager.addAccountExplicitly(account, mPassword, null);
197            // Set contacts sync for this account.
198            ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true);
199        } else {
200            mAccountManager.setPassword(account, mPassword);
201        }
202        final Intent intent = new Intent();
203        intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
204        intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
205        setAccountAuthenticatorResult(intent.getExtras());
206        setResult(RESULT_OK, intent);
207        finish();
208    }
209
210    /**
211     * Called when the authentication process completes (see attemptLogin()).
212     *
213     * @param authToken the authentication token returned by the server, or NULL if
214     *            authentication failed.
215     */
216    public void onAuthenticationResult(String authToken) {
217
218        boolean success = ((authToken != null) && (authToken.length() > 0));
219        Log.i(TAG, "onAuthenticationResult(" + success + ")");
220
221        // Our task is complete, so clear it out
222        mAuthTask = null;
223
224        // Hide the progress dialog
225        hideProgress();
226
227        if (success) {
228            if (!mConfirmCredentials) {
229                finishLogin(authToken);
230            } else {
231                finishConfirmCredentials(success);
232            }
233        } else {
234            Log.e(TAG, "onAuthenticationResult: failed to authenticate");
235            if (mRequestNewAccount) {
236                // "Please enter a valid username/password.
237                mMessage.setText(getText(R.string.login_activity_loginfail_text_both));
238            } else {
239                // "Please enter a valid password." (Used when the
240                // account is already in the database but the password
241                // doesn't work.)
242                mMessage.setText(getText(R.string.login_activity_loginfail_text_pwonly));
243            }
244        }
245    }
246
247    public void onAuthenticationCancel() {
248        Log.i(TAG, "onAuthenticationCancel()");
249
250        // Our task is complete, so clear it out
251        mAuthTask = null;
252
253        // Hide the progress dialog
254        hideProgress();
255    }
256
257    /**
258     * Returns the message to be displayed at the top of the login dialog box.
259     */
260    private CharSequence getMessage() {
261        getString(R.string.label);
262        if (TextUtils.isEmpty(mUsername)) {
263            // If no username, then we ask the user to log in using an
264            // appropriate service.
265            final CharSequence msg = getText(R.string.login_activity_newaccount_text);
266            return msg;
267        }
268        if (TextUtils.isEmpty(mPassword)) {
269            // We have an account but no password
270            return getText(R.string.login_activity_loginfail_text_pwmissing);
271        }
272        return null;
273    }
274
275    /**
276     * Shows the progress UI for a lengthy operation.
277     */
278    private void showProgress() {
279        showDialog(0);
280    }
281
282    /**
283     * Hides the progress UI for a lengthy operation.
284     */
285    private void hideProgress() {
286        if (mProgressDialog != null) {
287            mProgressDialog.dismiss();
288            mProgressDialog = null;
289        }
290    }
291
292    /**
293     * Represents an asynchronous task used to authenticate a user against the
294     * SampleSync Service
295     */
296    public class UserLoginTask extends AsyncTask<Void, Void, String> {
297
298        @Override
299        protected String doInBackground(Void... params) {
300            // We do the actual work of authenticating the user
301            // in the NetworkUtilities class.
302            try {
303                return NetworkUtilities.authenticate(mUsername, mPassword);
304            } catch (Exception ex) {
305                Log.e(TAG, "UserLoginTask.doInBackground: failed to authenticate");
306                Log.i(TAG, ex.toString());
307                return null;
308            }
309        }
310
311        @Override
312        protected void onPostExecute(final String authToken) {
313            // On a successful authentication, call back into the Activity to
314            // communicate the authToken (or null for an error).
315            onAuthenticationResult(authToken);
316        }
317
318        @Override
319        protected void onCancelled() {
320            // If the action was canceled (by the user clicking the cancel
321            // button in the progress dialog), then call back into the
322            // activity to let it know.
323            onAuthenticationCancel();
324        }
325    }
326}
327