/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at
* src/com/vodafone360/people/VODAFONE.LICENSE.txt or
* http://github.com/360/360-Engine-for-Android
* See the License for the specific language governing permissions and
* limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each file and
* include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the fields
* enclosed by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved.
* Use is subject to license terms.
*/
package com.vodafone360.people.engine.login;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.crypto.InvalidCipherTextException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import com.vodafone360.people.ApplicationCache;
import com.vodafone360.people.Intents;
import com.vodafone360.people.Settings;
import com.vodafone360.people.SettingsManager;
import com.vodafone360.people.database.DatabaseHelper;
import com.vodafone360.people.database.tables.StateTable;
import com.vodafone360.people.datatypes.AuthSessionHolder;
import com.vodafone360.people.datatypes.BaseDataType;
import com.vodafone360.people.datatypes.LoginDetails;
import com.vodafone360.people.datatypes.PublicKeyDetails;
import com.vodafone360.people.datatypes.RegistrationDetails;
import com.vodafone360.people.datatypes.SimpleText;
import com.vodafone360.people.engine.BaseEngine;
import com.vodafone360.people.engine.IEngineEventCallback;
import com.vodafone360.people.engine.EngineManager.EngineId;
import com.vodafone360.people.engine.contactsync.NativeContactsApi;
import com.vodafone360.people.service.ServiceStatus;
import com.vodafone360.people.service.ServiceUiRequest;
import com.vodafone360.people.service.agent.NetworkAgent;
import com.vodafone360.people.service.agent.UiAgent;
import com.vodafone360.people.service.agent.NetworkAgent.AgentState;
import com.vodafone360.people.service.io.ResponseQueue;
import com.vodafone360.people.service.io.api.Auth;
import com.vodafone360.people.service.receivers.SmsBroadcastReceiver;
import com.vodafone360.people.utils.LogUtils;
import com.vodafone360.people.utils.LoginPreferences;
import com.vodafone360.people.utils.SimCard;
/**
* Engine handling sign in to existing accounts and sign up to new accounts.
*/
public class LoginEngine extends BaseEngine {
/**
* List of states for LoginEngine.
*/
private enum State {
NOT_INITIALISED,
NOT_REGISTERED,
LOGGED_ON,
LOGGED_OFF,
LOGGED_OFF_WAITING_FOR_RETRY,
LOGGED_OFF_WAITING_FOR_NETWORK,
LOGIN_FAILED,
LOGIN_FAILED_WRONG_CREDENTIALS,
SIGNING_UP,
FETCHING_TERMS_OF_SERVICE,
FETCHING_PRIVACY_STATEMENT,
FETCHING_USERNAME_STATE,
REQUESTING_ACTIVATION_CODE,
CREATING_SESSION_MANUAL,
ACTIVATING_ACCOUNT,
CREATING_SESSION_AUTO,
RETRIEVING_PUBLIC_KEY
}
/**
* Text coming from the server contains these carriage return characters,
* which need to be exchanged with space characters to improve layout.
*/
private static final char CARRIAGE_RETURN_CHARACTER = (char) 13;
/**
* Text coming from the server contains carriage return characters, which
* need to be exchanged with these space characters to improve layout.
*/
private static final char SPACE_CHARACTER = (char) 32;
/**
* used for sending unsolicited ui events
*/
private final UiAgent mUiAgent = mEventCallback.getUiAgent();
/**
* mutex for thread synchronization
*/
private final Object mMutex = new Object();
/**
* To convert between seconds and milliseconds
*/
private static final int MS_IN_SECONDS = 1000;
/**
* Current state of the engine
*/
private State mState = State.NOT_INITIALISED;
/**
* Context used for listening to SMS events
*/
private Context mContext;
/**
* Database used for fetching/storing state information
*/
private DatabaseHelper mDb;
/**
* Contains the authenticated session information while the user is logged
* in
*/
private static AuthSessionHolder sActivatedSession = null;
/**
* Contains user login details such as user name and password
*/
private final LoginDetails mLoginDetails = new LoginDetails();
/**
* Contains a public key for encrypting login details to be sent to the
* server
*/
private PublicKeyDetails mPublicKey = new PublicKeyDetails();
/**
* Contains user registration details such as name, username, password, etc.
*/
private RegistrationDetails mRegistrationDetails = new RegistrationDetails();
/**
* Determines if current login information from database can be used to
* establish a session. Set to false if login fails repeatedly.
*/
private boolean mAreLoginDetailsValid = true;
/**
* Timeout used when waiting for the server to sent an activation SMS. If
* this time in milliseconds is exceeded, the login will fail with SMS not
* received error.
*/
private static final long ACTIVATE_LOGIN_TIMEOUT = 72000;
/**
* Holds the activation code once an activation SMS has been received.
*/
private String mActivationCode = null;
/**
* Set to true when sign in or sign up has been completed by the user. When
* this is false the landing page will be displayed when the application is
* launched.
*/
private boolean mIsRegistrationComplete = false;
/**
* Contains a list of login engine observers which will be notified when the
* login engine changes state.
*/
private final ArrayList<ILoginEventsListener> mEventsListener = new ArrayList<ILoginEventsListener>();
/**
* Determines if the user is currently logged in with a valid session
*/
private boolean mCurrentLoginState = false;
/**
* Listener interface that can be used by clients to receive login state
* events from the engine.
*/
public static interface ILoginEventsListener {
void onLoginStateChanged(boolean loggedIn);
}
/**
* Public constructor.
*
* @param context The service Context
* @param eventCallback Provides access to useful engine manager
* functionality
* @param db The Now+ database used for fetching/storing login state
* information
*/
public LoginEngine(Context context, IEngineEventCallback eventCallback, DatabaseHelper db) {
super(eventCallback);
LogUtils.logD("LoginEngine.LoginEngine()");
mContext = context;
mDb = db;
mEngineId = EngineId.LOGIN_ENGINE;
}
/**
* This will be called immediately after creation to perform extra
* initialisation.
*/
@Override
public void onCreate() {
LogUtils.logD("LoginEngine.OnCreate()");
mState = State.NOT_INITIALISED;
IntentFilter filter = new IntentFilter(SmsBroadcastReceiver.ACTION_ACTIVATION_CODE);
mContext.registerReceiver(mEventReceiver, filter);
mIsRegistrationComplete = StateTable.isRegistrationComplete(mDb.getReadableDatabase());
initializeEngine();
}
/**
* This will be called just before the engine is shutdown. Cleans up any
* resources used.
*/
@Override
public void onDestroy() {
LogUtils.logD("LoginEngine.onDestroy()");
// Intent intent = new Intent();
// intent.setAction(Intents.HIDE_LOGIN_NOTIFICATION);
// mContext.sendBroadcast(intent);
removeAllListeners();
mContext.unregisterReceiver(mEventReceiver);
}
/**
* Called by the INowPlusService implementation to start a manual login.
* Adds a manual login UI request to the queue and kicks the worker thread.
*
* @param loginDetails The login information entered by the user
*/
public void addUiLoginRequest(LoginDetails loginDetails) {
LogUtils.logD("LoginEngine.addUiLoginRequest()");
addUiRequestToQueue(ServiceUiRequest.LOGIN, loginDetails);
}
/**
* Called by the INowPlusService implementation to remove user data Issues a
* UI request to effectively log out.
*/
public void addUiRemoveUserDataRequest() {
LogUtils.logD("LoginEngine.addUiRemoveUserDataRequest()");
addUiRequestToQueue(ServiceUiRequest.REMOVE_USER_DATA, null);
}
/**
* Called by the INowPlusService implementation to start the sign-up
* process. Adds a registration UI request to the queue and kicks the worker
* thread.
*
* @param details The registration details entered by the user
*/
public void addUiRegistrationRequest(RegistrationDetails details) {
LogUtils.logD("LoginEngine.addUiRegistrationRequest()");
addUiRequestToQueue(ServiceUiRequest.REGISTRATION, details);
}
/**
* Called by the INowPlusService implementation to fetch terms of service
* text for the UI. Adds a fetch terms of service UI request to the queue
* and kicks the worker thread.
*/
public void addUiFetchTermsOfServiceRequest() {
LogUtils.logD("LoginEngine.addUiFetchTermsOfServiceRequest()");
addUiRequestToQueue(ServiceUiRequest.FETCH_TERMS_OF_SERVICE, null);
}
/**
* Called by the INowPlusService implementation to privacy statement for the
* UI. Adds a fetch privacy statement UI request to the queue and kicks the
* worker thread.
*/
public void addUiFetchPrivacyStatementRequest() {
LogUtils.logD("LoginEngine.addUiFetchPrivacyStatementRequest()");
addUiRequestToQueue(ServiceUiRequest.FETCH_PRIVACY_STATEMENT, null);
}
/**
* Called by the INowPlusService implementation to check if a username is
* available for registration. Adds a get username state UI request to the
* queue and kicks the worker thread.
*
* @param username Username to fetch the state of
* TODO: Not currently used by UI.
*/
public void addUiGetUsernameStateRequest(String username) {
LogUtils.logD("LoginEngine.addUiGetUsernameStateRequest()");
addUiRequestToQueue(ServiceUiRequest.USERNAME_AVAILABILITY, username);
}
/**
* Return the absolute time in milliseconds when the engine needs to run
* (based on System.currentTimeMillis).
*
* @return -1 never needs to run, 0 needs to run as soon as possible,
* CurrentTime + 60000 to run in 1 minute, etc.
*/
@Override
public long getNextRunTime() {
if (isCommsResponseOutstanding()) {
return 0;
}
if (uiRequestReady()) {
return 0;
}
switch (mState) {
case NOT_INITIALISED:
return 0;
case NOT_REGISTERED:
case LOGGED_ON:
case LOGIN_FAILED:
case LOGIN_FAILED_WRONG_CREDENTIALS:
return -1;
case REQUESTING_ACTIVATION_CODE:
case SIGNING_UP:
if (mActivationCode != null) {
return 0;
}
break;
case LOGGED_OFF:
return 0;
case LOGGED_OFF_WAITING_FOR_NETWORK:
if (NetworkAgent.getAgentState() == AgentState.CONNECTED) {
return 0;
}
break;
case RETRIEVING_PUBLIC_KEY:
return 0;
default:
// Do nothing.
break;
}
return getCurrentTimeout();
}
/**
* Do some work but anything that takes longer than 1 second must be broken
* up. The type of work done includes:
* <ul>
* <li>If a comms response from server is outstanding, process it</li>
* <li>If a timeout is pending, process it</li>
* <li>If an SMS activation code has been received, process it</li>
* <li>Retry auto login if necessary</li>
* </ul>
*/
@Override
public void run() {
LogUtils.logD("LoginEngine.run()");
if (isCommsResponseOutstanding() && processCommsInQueue()) {
return;
}
if (processTimeout()) {
return;
}
switch (mState) {
case NOT_INITIALISED:
initializeEngine();
return;
case REQUESTING_ACTIVATION_CODE:
case SIGNING_UP:
if (mActivationCode != null) {
handleSmsResponse();
return;
}
break;
case RETRIEVING_PUBLIC_KEY:
break;
case LOGGED_OFF:
case LOGGED_OFF_WAITING_FOR_NETWORK:
AuthSessionHolder session = StateTable.fetchSession(mDb.getReadableDatabase());
// if session is null we try to login automatically again
if (null == session) {
if (retryAutoLogin()) {
return;
}
} else { // otherwise we try to reuse the session
sActivatedSession = session;
newState(State.LOGGED_ON);
}
default: // do nothing.
break;
}
if (uiRequestReady() && processUiQueue()) {
return;
}
}
/**
* Helper function which returns true if a UI request is waiting on the
* queue and we are ready to process it.
*
* @return true if the request can be processed now.
*/
private boolean uiRequestReady() {
if (!isUiRequestOutstanding()) {
return false;
}
switch (mState) {
case NOT_REGISTERED:
case LOGGED_OFF:
case LOGGED_ON:
case LOGIN_FAILED:
case CREATING_SESSION_AUTO:
case LOGIN_FAILED_WRONG_CREDENTIALS:
case LOGGED_OFF_WAITING_FOR_RETRY:
return true;
default:
return false;
}
}
/**
* Determines if the user is currently logged in with a valid session.
*
* @return true if logged in, false otherwise
*/
public boolean isLoggedIn() {
return mState == State.LOGGED_ON;
}
/**
* Add a listener which will receive events whenever the login state
* changes.
*
* @param listener The callback interface
*/
public synchronized void addListener(ILoginEventsListener listener) {
LogUtils.logD("LoginEngine.addListener()");
if (!mEventsListener.contains(listener)) {
mEventsListener.add(listener);
}
}
/**
* Remove a listener added by the addListener function.
*
* @param listener The same callback interface passed in to the add function
*/
public synchronized void removeListener(ILoginEventsListener listener) {
LogUtils.logD("LoginEngine.removeListener()");
mEventsListener.remove(listener);
}
/**
* Remove all ILoginStateChangeListener (done as part of cleanup).
*/
private synchronized void removeAllListeners() {
LogUtils.logD("LoginEngine.removeAllListeners()");
if (mEventsListener != null) {
mEventsListener.clear();
}
}
/**
* Once the engine has finished processing a user request, this function is
* called to restore the state back to an appropriate value (based on the
* user login state).
*/
private synchronized void restoreLoginState() {
LogUtils.logD("LoginEngine.restoreLoginState");
if (mIsRegistrationComplete) {
if (sActivatedSession != null) {
newState(State.LOGGED_ON);
} else {
if (mAreLoginDetailsValid) {
newState(State.LOGGED_OFF);
} else {
newState(State.LOGIN_FAILED);
}
}
} else {
newState(State.NOT_REGISTERED);
}
}
/**
* Called when a server response is received, processes the response based
* on the engine state.
*
* @param resp Response data from server
*/
@Override
protected void processCommsResponse(ResponseQueue.DecodedResponse resp) {
LogUtils.logD("LoginEngine.processCommsResponse() - resp = " + resp);
switch (mState) {
case SIGNING_UP:
handleSignUpResponse(resp.mDataTypes);
break;
case RETRIEVING_PUBLIC_KEY:
handleNewPublicKeyResponse(resp.mDataTypes);
break;
case CREATING_SESSION_MANUAL:
handleCreateSessionManualResponse(resp.mDataTypes);
break;
case CREATING_SESSION_AUTO:
handleCreateSessionAutoResponse(resp.mDataTypes);
break;
case REQUESTING_ACTIVATION_CODE:
handleRequestingActivationResponse(resp.mDataTypes);
break;
case ACTIVATING_ACCOUNT:
handleActivateAccountResponse(resp.mDataTypes);
break;
case FETCHING_TERMS_OF_SERVICE:
case FETCHING_PRIVACY_STATEMENT:
case FETCHING_USERNAME_STATE:
handleServerSimpleTextResponse(resp.mDataTypes, mState);
break;
default: // do nothing.
break;
}
}
/**
* Called when a UI request is ready to be processed. Handlers the UI
* request based on the type.
*
* @param requestId UI request type
* @param data Interpretation of this data depends on the request type
*/
@Override
protected void processUiRequest(ServiceUiRequest requestId, Object data) {
LogUtils.logD("LoginEngine.processUiRequest() - reqID = " + requestId);
switch (requestId) {
case LOGIN:
startManualLoginProcess((LoginDetails)data);
break;
case REGISTRATION:
startRegistrationProcessCrypted((RegistrationDetails)data);
break;
case REMOVE_USER_DATA:
startLogout();
// Remove NAB Account at this point (does nothing on 1.X)
NativeContactsApi.getInstance().removePeopleAccount();
super.onReset(); // Sets the reset flag as done
break;
case LOGOUT:
startLogout();
break;
case FETCH_TERMS_OF_SERVICE:
startFetchTermsOfService();
break;
case FETCH_PRIVACY_STATEMENT:
startFetchPrivacyStatement();
break;
case USERNAME_AVAILABILITY:
startFetchUsernameState((String)data);
break;
default:
completeUiRequest(ServiceStatus.ERROR_NOT_FOUND, null);
}
}
/**
* Called by the run() function the first time it is executed to perform
* non-trivial initialisation such as auto login.
*/
private void initializeEngine() {
LogUtils.logD("LoginEngine.initialiseEngine()");
if (ServiceStatus.SUCCESS == mDb.fetchLogonCredentialsAndPublicKey(mLoginDetails,
mPublicKey)) {
sActivatedSession = StateTable.fetchSession(mDb.getReadableDatabase());
}
mAreLoginDetailsValid = true;
restoreLoginState();
clearTimeout();
}
/**
* Starts the sign-up process where the password is RSA encrypted. A setting
* determines if this function is used in preference to
* {@link #startRegistrationProcess(RegistrationDetails)} function.
*
* @param details Registration details received from the UI request
*/
private void startRegistrationProcessCrypted(RegistrationDetails details) {
LogUtils.logD("startRegistrationCrypted");
if (details == null || details.mUsername == null || details.mPassword == null) {
completeUiRequest(ServiceStatus.ERROR_BAD_SERVER_PARAMETER, null);
return;
}
setRegistrationComplete(false);
setActivatedSession(null);
mRegistrationDetails.copy(details);
mLoginDetails.mUsername = mRegistrationDetails.mUsername;
mLoginDetails.mPassword = mRegistrationDetails.mPassword;
try {
final long timestampInSeconds = System.currentTimeMillis() / MS_IN_SECONDS;
final byte[] theBytes = prepareBytesForSignup(timestampInSeconds);
mLoginDetails.mAutoConnect = true;
mLoginDetails.mRememberMe = true;
mLoginDetails.mMobileNo = mRegistrationDetails.mMsisdn;
mLoginDetails.mSubscriberId = SimCard.getSubscriberId(mContext);
mDb.modifyCredentialsAndPublicKey(mLoginDetails, mPublicKey);
startSignUpCrypted(theBytes, timestampInSeconds);
} catch (InvalidCipherTextException e) {
e.printStackTrace();
}
}
/**
* Encrypts the sign-up data ready for sending to the server
*
* @param timeStampInSeconds Current time in milliseconds
* @return Raw data that can be sent to the server
*/
private byte[] prepareBytesForSignup(long timeStampInSeconds) throws InvalidCipherTextException {
byte[] theBytes = null;
if (mPublicKey != null) {
if (mPublicKey.mExponential != null && (mPublicKey.mModulus != null)) {
theBytes = RSAEncryptionUtils.encryptRSA(RSAEncryptionUtils.getRSAPubKey(
mPublicKey.mModulus, mPublicKey.mExponential), makeSecurePassword(
mLoginDetails.mUsername, mLoginDetails.mPassword, timeStampInSeconds));
}
}
if (theBytes == null) {
RSAEncryptionUtils.copyDefaultPublicKey(mPublicKey);// we'll store
// the default
// public key
// into the db
theBytes = RSAEncryptionUtils.encryptRSA(RSAEncryptionUtils.getDefaultPublicKey(),
makeSecurePassword(mLoginDetails.mUsername, mLoginDetails.mPassword,
timeStampInSeconds));
}
return theBytes;
}
/**
* Concatenates the username, password, current time and some other data
* into a string which can be encrypted and sent to the server.
*
* @param userName User name for sign-in or sign-up
* @param password Password as entered by user
* @param ts Current time in milliseconds
* @return Concatenated data
*/
private static String makeSecurePassword(String userName, String password, long ts) {
String appSecret = SettingsManager.getProperty(Settings.APP_SECRET_KEY);// RSAEncrypter.testAppSecretThrottled;
final char amp = '&';
if (ts <= 0 || //
userName == null || userName.trim().length() == 0 || //
password == null || password.trim().length() == 0 || //
// set application key somewhere
appSecret == null || appSecret.trim().length() == 0)
return null;
final String passwordT = password.trim();
final String userNameT = userName.trim();
final StringBuffer sb = new StringBuffer();
sb.append(appSecret).append(amp).append(Long.toString(ts)).append(amp).append(userNameT)
.append(amp).append(passwordT);
return sb.toString();
}
/**
* Puts the engine into the signing up state and sends an encrypted sign-up
* request to the server.
*
* @param theBytes byte-array containing encrypted password data.
* @param timestamp Current timestamp.
*/
private void startSignUpCrypted(byte[] theBytes, long timestamp) {
LogUtils.logD("startSignUpCrypted()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
mActivationCode = null;
newState(State.SIGNING_UP);
if (!validateRegistrationDetails()) {
completeUiRequest(ServiceStatus.ERROR_BAD_SERVER_PARAMETER, null);
return;
}
int reqId = Auth.signupUserCrypted(
this,
mRegistrationDetails.mFullname,
mRegistrationDetails.mUsername,
theBytes, // what is encrypted
timestamp, mRegistrationDetails.mEmail, mRegistrationDetails.mBirthdayDate,
mRegistrationDetails.mMsisdn, mRegistrationDetails.mAcceptedTAndC,
mRegistrationDetails.mCountrycode, mRegistrationDetails.mTimezone,
mRegistrationDetails.mLanguage, mRegistrationDetails.mMobileOperatorId,
mRegistrationDetails.mMobileModelId, mRegistrationDetails.mSendConfirmationMail,
mRegistrationDetails.mSendConfirmationSms,
mRegistrationDetails.mSubscribeToNewsLetter);
if (!setReqId(reqId)) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Basic check to determine if the registration details given by the user
* are valid
*
* @return true if the details are valid, false otherwise.
*/
private boolean validateRegistrationDetails() {
if (mRegistrationDetails.mFullname == null || mRegistrationDetails.mEmail == null
|| mRegistrationDetails.mBirthdayDate == null
|| mRegistrationDetails.mMsisdn == null
|| mRegistrationDetails.mAcceptedTAndC == null
|| mRegistrationDetails.mCountrycode == null
|| mRegistrationDetails.mTimezone == null || mRegistrationDetails.mLanguage == null
|| mRegistrationDetails.mMobileOperatorId == null
|| mRegistrationDetails.mMobileModelId == null
|| mRegistrationDetails.mSendConfirmationMail == null
|| mRegistrationDetails.mSendConfirmationSms == null
|| mRegistrationDetails.mSubscribeToNewsLetter == null) {
return false;
}
return true;
}
/**
* Requests a new Public Key from the server.
*/
public void getNewPublicKey() {
LogUtils.logD("LoginEngine.getNewPublicKey");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
mActivationCode = null;
newState(State.RETRIEVING_PUBLIC_KEY);
if (!setReqId(Auth.getPublicKey(this))) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sends a fetch terms of service request to the server.
*/
private void startFetchTermsOfService() {
LogUtils.logD("LoginEngine.startFetchTermsOfService()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
updateTermsState(ServiceStatus.ERROR_COMMS, null);
return;
}
newState(State.FETCHING_TERMS_OF_SERVICE);
if (!setReqId(Auth.getTermsAndConditions(this))) {
updateTermsState(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sends a fetch privacy statement request to the server.
*/
private void startFetchPrivacyStatement() {
LogUtils.logD("LoginEngine.startFetchPrivacyStatement()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
updateTermsState(ServiceStatus.ERROR_COMMS, null);
return;
}
newState(State.FETCHING_PRIVACY_STATEMENT);
if (!setReqId(Auth.getPrivacyStatement(this))) {
updateTermsState(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sends a fetch user-name state request to the server.
*
* @param username, the user-name to retrieve information for.
*/
private void startFetchUsernameState(String username) {
LogUtils.logD("LoginEngine.startFetchUsernameState()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
mLoginDetails.mUsername = username;
newState(State.FETCHING_USERNAME_STATE);
if (!setReqId(Auth.getUsernameState(this, username))) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sets registration complete flag to false then starts a new sign-in
* request.
*
* @param details Login details received from the UI request
*/
private void startManualLoginProcess(LoginDetails details) {
LogUtils.logD("LoginEngine.startManualLoginProcess()");
setRegistrationComplete(false);
setActivatedSession(null);
if (details == null) {
completeUiRequest(ServiceStatus.ERROR_BAD_SERVER_PARAMETER, null);
return;
}
mLoginDetails.copy(details);
mLoginDetails.mSubscriberId = SimCard.getSubscriberId(mContext);
mDb.modifyCredentialsAndPublicKey(mLoginDetails, mPublicKey);
if (Settings.ENABLE_ACTIVATION) {
startRequestActivationCode();
} else {
startGetSessionManual();
}
}
/**
* Sends a request activation code request to the server.
*/
private void startRequestActivationCode() {
LogUtils.logD("LoginEngine.startRequestActivationCode()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
mActivationCode = null;
newState(State.REQUESTING_ACTIVATION_CODE);
if (!setReqId(Auth.requestActivationCode(this, mLoginDetails.mUsername,
mLoginDetails.mMobileNo))) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sends a get session by credentials request to the server.
*/
private void startGetSessionManual() {
LogUtils.logD("LoginEngine.startGetSessionManual()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
newState(State.CREATING_SESSION_MANUAL);
if (!setReqId(Auth.getSessionByCredentials(this, mLoginDetails.mUsername,
mLoginDetails.mPassword, null))) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
}
/**
* Sends a activate account request to the server.
*/
private void startActivateAccount() {
LogUtils.logD("LoginEngine.startActivateAccount()");
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
completeUiRequest(NetworkAgent.getServiceStatusfromDisconnectReason(), null);
return;
}
if (Settings.ENABLE_ACTIVATION) {
newState(State.ACTIVATING_ACCOUNT);
if (!setReqId(Auth.activate(this, mActivationCode))) {
completeUiRequest(ServiceStatus.ERROR_COMMS, null);
return;
}
} else {
setRegistrationComplete(true);
completeUiRequest(ServiceStatus.SUCCESS, null);
}
}
/**
* Clears the session and registration complete information so that the user
* will need to manually login again to use the now+ services. Does not
* currently send a request to the server to log out.
*/
private void startLogout() {
LogUtils.logD("LoginEngine.startLogout()");
setRegistrationComplete(false);
setActivatedSession(null);
completeUiRequest(ServiceStatus.SUCCESS, null);
}
/**
* Clears the session and registration complete information so that the user
* will need to manually login again to use the now+ services. Does not
* currently send a request to the server to log out.
*/
public final void logoutAndRemoveUser() {
LogUtils.logD("LoginEngine.startLogout()");
addUiRemoveUserDataRequest();
LoginPreferences.clearPreferencesFile(mContext);
mUiAgent.sendUnsolicitedUiEvent(ServiceUiRequest.UNSOLICITED_GO_TO_LANDING_PAGE, null);
}
/**
* Retries to log the user in based on credential information stored in the
* database.
*
* @return true if the login process was able to start
*/
public boolean retryAutoLogin() {
LogUtils.logD("LoginEngine.retryAutoLogin()");
setActivatedSession(null);
if (ServiceStatus.SUCCESS != mDb.fetchLogonCredentialsAndPublicKey(mLoginDetails,
mPublicKey)) {
LogUtils
.logE("LoginEngine.retryAutoLogin() - Unable to fetch credentials from database");
mAreLoginDetailsValid = false;
newState(State.LOGIN_FAILED);
return false;
}
// AA: commented the condition out if (Settings.ENABLE_ACTIVATION) {
// AA: the old version if (mCurrentSubscriberId == null ||
// !mCurrentSubscriberId.equals(mLoginDetails.mSubscriberId)) { //
// logging off/fail will be done in another way according to bug 8288
final String currentSubscriberId = SimCard.getSubscriberId(mContext);
if (currentSubscriberId != null
&& !currentSubscriberId.equals(mLoginDetails.mSubscriberId)) {
LogUtils.logV("LoginEngine.retryAutoLogin() -"
+ " SIM card has changed or is missing (old subId = "
+ mLoginDetails.mSubscriberId + ", new subId = " + currentSubscriberId + ")");
mAreLoginDetailsValid = false;
newState(State.LOGIN_FAILED);
return false;
}
// }
if (mLoginDetails.mUsername == null || mLoginDetails.mPassword == null
|| mLoginDetails.mMobileNo == null) {
LogUtils.logV("LoginEngine.retryAutoLogin() - Username, password "
+ "or mobile number are missing (old username = " + mLoginDetails.mUsername
+ ", mobile no = " + mLoginDetails.mMobileNo + ")");
mAreLoginDetailsValid = false;
newState(State.LOGIN_FAILED);
return false;
}
mAreLoginDetailsValid = true;
if (NetworkAgent.getAgentState() != NetworkAgent.AgentState.CONNECTED) {
newState(State.LOGGED_OFF_WAITING_FOR_NETWORK);
LogUtils.logV("LoginEngine.retryAutoLogin() - Internet connection down. "
+ "Will try again when connection is available");
return false;
}
newState(State.CREATING_SESSION_AUTO);
if (!setReqId(Auth.getSessionByCredentials(this, mLoginDetails.mUsername,
mLoginDetails.mPassword, null))) {
return false;
}
return true;
}
/**
* Helper function to set the registration complete flag and update the
* database with the new state.
*
* @param value true if registration is completed
*/
private void setRegistrationComplete(boolean value) {
LogUtils.logD("LoginEngine.setRegistrationComplete(" + value + ")");
if (value != mIsRegistrationComplete) {
StateTable.setRegistrationComplete(value, mDb.getWritableDatabase());
mIsRegistrationComplete = value;
if (mIsRegistrationComplete) {
// Create NAB Account at this point (does nothing on 1.X
// devices)
final NativeContactsApi nabApi = NativeContactsApi.getInstance();
if (!nabApi.isPeopleAccountCreated()) {
// TODO: React upon failure to create account
nabApi.addPeopleAccount(LoginPreferences.getUsername());
}
}
}
}
/**
* Changes the state of the engine. Also displays the login notification if
* necessary.
*
* @param newState The new state
*/
private void newState(State newState) {
State oldState = mState;
synchronized (mMutex) {
if (newState == mState) {
return;
}
mState = newState;
}
LogUtils.logV("LoginEngine.newState: " + oldState + " -> " + mState);
Intent intent = null;
// Update notification
switch (mState) {
case LOGIN_FAILED_WRONG_CREDENTIALS:
intent = new Intent();
intent.setAction(Intents.START_LOGIN_ACTIVITY);
mContext.sendBroadcast(intent);
setRegistrationComplete(false);
break;
// here should be no break
case NOT_REGISTERED:
case LOGIN_FAILED:
// intent = new Intent();
// intent.setAction(Intents.LOGIN_FAILED);
// mContext.sendBroadcast(intent);
setRegistrationComplete(false);
// startLogout();
// mDb.removeUserData();
// sending user to login screen again
// should be done by UI itself because
// when it's done from here it cause problems when user tries to
// login
// giving wrong credentials, ui flow will be broken
break;
case LOGGED_OFF:
case LOGGED_OFF_WAITING_FOR_NETWORK:
case LOGGED_OFF_WAITING_FOR_RETRY:
case LOGGED_ON:
// intent = new Intent();
// intent.setAction(Intents.HIDE_LOGIN_NOTIFICATION);
// mContext.sendBroadcast(intent);
break;
default:// do nothing
break;
}
// Update listeners with any state changes
switch (mState) {
case NOT_REGISTERED:
case LOGIN_FAILED:
case LOGIN_FAILED_WRONG_CREDENTIALS:
case LOGGED_OFF:
onLoginStateChanged(false);
break;
case LOGGED_ON:
onLoginStateChanged(true);
break;
default: // do nothing.
break;
}
}
/**
* Called when the engine transitions between the logged in and logged out
* states. Notifies listeners.
*
* @param loggedIn true if the user is now logged in, false otherwise.
*/
private synchronized void onLoginStateChanged(boolean loggedIn) {
LogUtils.logD("LoginEngine.onLoginStateChanged() Login state changed to "
+ (loggedIn ? "logged in." : "logged out."));
if (loggedIn == mCurrentLoginState) {
return;
}
mCurrentLoginState = loggedIn;
for (ILoginEventsListener listener : mEventsListener) {
listener.onLoginStateChanged(loggedIn);
}
}
/**
* A helper function which determines which activity should be displayed
* when the UI is loaded.
*
* @return true if landing page should be displayed, false otherwise
*/
public synchronized boolean getLoginRequired() {
LogUtils.logD("LoginEngine.getLoginRequired() - " + !mIsRegistrationComplete);
return !mIsRegistrationComplete;
}
/**
* Retrieves the active comms session.
*
* @return The session or NULL if the user is logged out.
*/
public static AuthSessionHolder getSession() {
return sActivatedSession;
}
/**
* Helper function to store the new session in the database and inform
* clients that the session has changed.
*
* @param session The new session or NULL if the user has logged off.
*/
public synchronized void setActivatedSession(AuthSessionHolder session) {
LogUtils.logD("LoginEngine.setActivatedSession() session[" + session + "]");
sActivatedSession = session;
if (session != null) {
LogUtils.logD("LoginEngine.setActivatedSession() Login successful");
} else {
LogUtils.logW("LoginEngine.setActivatedSession() "
+ "Login unsuccessful, the session is NULL");
}
StateTable.setSession(session, mDb.getWritableDatabase());
}
/**
* Called when a response to the sign-up API is received. In case of success
* sets a timeout value waiting for the activation SMS to arrive.
*
* @param data The received data
*/
private void handleSignUpResponse(List<BaseDataType> data) {
ServiceStatus errorStatus = getResponseStatus(BaseDataType.CONTACT_DATA_TYPE, data);
LogUtils.logD("LoginEngine.handleSignUpResponse() errorStatus[" + errorStatus.name() + "]");
if (errorStatus == ServiceStatus.SUCCESS) {
LogUtils.logD("LoginEngine.handleSignUpResponse() - Registration successful");
if (!Settings.ENABLE_ACTIVATION) {
startGetSessionManual();
} else {
// Now waiting for SMS...
setTimeout(ACTIVATE_LOGIN_TIMEOUT);
}
// AA
} else if (errorStatus == ServiceStatus.ERROR_INVALID_PUBLIC_KEY) {
// start new key retrieval and make the new cycle
getNewPublicKey();
} else {
completeUiRequest(errorStatus, null);
}
}
/**
* Called when a response to the server fetch public key request is
* received. Validates the response and stores the new public key details.
*
* @param mDataTypes Response data from server.
*/
private void handleNewPublicKeyResponse(List<BaseDataType> mDataTypes) {
LogUtils.logD("LoginEngine.handleNewPublicKeyResponse()");
ServiceStatus errorStatus = getResponseStatus(BaseDataType.PUBLIC_KEY_DETAILS_DATA_TYPE, mDataTypes);
if (errorStatus == ServiceStatus.SUCCESS) {
LogUtils.logD("LoginEngine.handleNewPublicKeyResponse() - Succesfully retrieved");
// AA
// 1. save to DB; save the flag that we aren't using default and
// have to use one from DB
// 2. start registration again
mPublicKey = (PublicKeyDetails)mDataTypes.get(0);
// done in startRegistrationProcessCrypted already
// mDb.modifyCredentialsAndPublicKey(mLoginDetails, mPublicKey);
startRegistrationProcessCrypted(mRegistrationDetails);
} else {
completeUiRequest(errorStatus, null);
}
}
/**
* Called when a response to the GetSessionByCredentials API is received
* (manual login). In case of success, tries to activate the account using
* the activation code received by SMS.
*
* @param data The received data
*/
private void handleCreateSessionManualResponse(List<BaseDataType> data) {
LogUtils.logD("LoginEngine.handleCreateSessionManualResponse()");
ServiceStatus errorStatus = getResponseStatus(BaseDataType.AUTH_SESSION_HOLDER_TYPE, data);
if (errorStatus == ServiceStatus.SUCCESS && (data.size() > 0)) {
setActivatedSession((AuthSessionHolder)data.get(0));
startActivateAccount();
return;
}
completeUiRequest(errorStatus, null);
}
/**
* Called when a response to the GetSessionByCredentials API is received
* (auto login). In case of success, moves to the logged in state
*
* @param data The received data
*/
private void handleCreateSessionAutoResponse(List<BaseDataType> data) {
LogUtils.logD("LoginEngine.handleCreateSessionResponse()");
ServiceStatus errorStatus = getResponseStatus(BaseDataType.AUTH_SESSION_HOLDER_TYPE, data);
if (errorStatus == ServiceStatus.SUCCESS && (data.size() > 0)) {
clearTimeout();
setActivatedSession((AuthSessionHolder)data.get(0));
newState(State.LOGGED_ON);
} else {
LogUtils.logE("LoginEngine.handleCreateSessionResponse() - Auto Login Failed Error "
+ errorStatus);
// AA:the 1st retry failed, just go to the start page,
// if (loginAttemptsRemaining()
// && errorStatus!=ServiceStatus.ERROR_INVALID_PASSWORD) {
// newState(State.LOGGED_OFF_WAITING_FOR_RETRY);
// setTimeout(LOGIN_RETRY_TIME);
// } else {
// mAreLoginDetailsValid = false;
if (errorStatus == ServiceStatus.ERROR_INVALID_PASSWORD) {
mAreLoginDetailsValid = false;
newState(State.LOGIN_FAILED_WRONG_CREDENTIALS);
} else {
newState(State.LOGIN_FAILED);
}
// }
}
}
/**
* Called when a response to the RequestActivationCode API is received
* (manual login). In case of success, tries to fetch a login session from
* the server.
*
* @param data The received data
*/
private void handleRequestingActivationResponse(List<BaseDataType> data) {
LogUtils.logD("LoginEngine.handleRequestingActivationResponse()");
ServiceStatus errorStatus = getResponseStatus(BaseDataType.STATUS_MSG_DATA_TYPE, data);
if (errorStatus == ServiceStatus.SUCCESS) {
// Now waiting for SMS...
setTimeout(ACTIVATE_LOGIN_TIMEOUT);
return;
}
completeUiRequest(errorStatus, null);
}
/**
* Called when a response to the Activate API is received (manual login or
* sign-up). In case of success, moves to the logged in state and completes
* the manual login or sign-up request.
*
* @param data The received data
*/
private void handleActivateAccountResponse(List<BaseDataType> data) {
LogUtils.logD("LoginEngine.handleActivateAccountResponse()");
ServiceStatus errorStatus = getResponseStatus(BaseDataType.STATUS_MSG_DATA_TYPE, data);
if (errorStatus == ServiceStatus.SUCCESS) {
LogUtils
.logD("LoginEngine.handleActivateAccountResponse: ** Mobile number activated **");
setRegistrationComplete(true);
completeUiRequest(ServiceStatus.SUCCESS, null);
return;
}
setActivatedSession(null);
completeUiRequest(ServiceStatus.ERROR_ACCOUNT_ACTIVATION_FAILED, null);
}
/**
* Called when a response to the GetTermsAndConditions, GetPrivacyStatement
* and GetUsernameState APIs are received. In case of success, completes the
* request and passes the response data to the UI.
*
* @param data The received data
*/
private void handleServerSimpleTextResponse(List<BaseDataType> data, State type) {
LogUtils.logV("LoginEngine.handleServerSimpleTextResponse()");
ServiceStatus serviceStatus = getResponseStatus(BaseDataType.SIMPLE_TEXT_DATA_TYPE, data);
String result = null;
if (serviceStatus == ServiceStatus.SUCCESS) {
result = ((SimpleText) data.get(0)).mValue.toString().replace(
CARRIAGE_RETURN_CHARACTER, SPACE_CHARACTER);
switch (type) {
case FETCHING_TERMS_OF_SERVICE:
LogUtils.logD("LoginEngine.handleServerSimpleTextResponse() Terms of Service");
ApplicationCache.setTermsOfService(result, mContext);
break;
case FETCHING_PRIVACY_STATEMENT:
LogUtils.logD("LoginEngine.handleServerSimpleTextResponse() Privacy Statemet");
ApplicationCache.setPrivacyStatemet(result, mContext);
break;
case FETCHING_USERNAME_STATE:
// TODO: Unused by UI.
break;
}
}
updateTermsState(serviceStatus, result);
}
/***
* Informs the UI to update any terms which are being shown on screen.
*
* @param serviceStatus Current ServiceStatus.
* @param messageText Legacy call for old UI (TODO: remove after UI-Refresh
* merge). NULL when combined with a ServiceStatus of
* ERROR_COMMS, or contains the Privacy or Terms and Conditions
* text to be displayed in the UI.
*/
private void updateTermsState(ServiceStatus serviceStatus, String messageText) {
ApplicationCache.setTermsStatus(serviceStatus);
/** Trigger UiAgent. **/
mUiAgent.sendUnsolicitedUiEvent(ServiceUiRequest.TERMS_CHANGED_EVENT, null);
/** Clear this request from the UI queue. **/
completeUiRequest(serviceStatus, messageText);
}
/**
* A broadcast receiver which is used to receive notifications when a data
* SMS arrives.
*/
private final BroadcastReceiver mEventReceiver = new BroadcastReceiver() {
/**
* Called when an SMS arrives. The activation code is extracted from the
* given intent and the worker thread kicked.
*
* @param context Context from which the intent was broadcast
* @param intent Will only process the SMS which the action is
* {@link SmsBroadcastReceiver#ACTION_ACTIVATION_CODE}.
*/
@Override
public void onReceive(Context context, Intent intent) {
LogUtils.logD("LoginEngine.BroadcastReceiver.onReceive - Processing data sms");
if (intent.getAction().equals(SmsBroadcastReceiver.ACTION_ACTIVATION_CODE)) {
String activationCode = intent.getStringExtra("code");
LogUtils
.logD("LoginEngine.BroadcastReceiver.onReceive - Activation code Received: "
+ activationCode);
synchronized (LoginEngine.this) {
mActivationCode = activationCode;
}
mEventCallback.kickWorkerThread();
}
}
};
/**
* Called when an SMS is received during sign-up or manual login. Starts
* requesting a session from the server.
*/
private void handleSmsResponse() {
LogUtils.logD("LoginEngine.handleSmsResponse(" + mActivationCode + ")");
clearTimeout();
startGetSessionManual();
}
/**
* Called by the base engine implementation whenever a UI request is
* completed to do any necessary cleanup. We use it to restore our state to
* a suitable value.
*/
@Override
protected void onRequestComplete() {
LogUtils.logD("LoginEngine.onRequestComplete()");
restoreLoginState();
}
/**
* Handles timeouts for SMS activation and auto login retries.
*/
protected synchronized void onTimeoutEvent() {
LogUtils.logD("LoginEngine.onTimeoutEvent()");
switch (mState) {
case REQUESTING_ACTIVATION_CODE:
case SIGNING_UP:
completeUiRequest(ServiceStatus.ERROR_SMS_CODE_NOT_RECEIVED, null);
break;
case LOGGED_OFF_WAITING_FOR_RETRY:
retryAutoLogin();
break;
default: // do nothing.
break;
}
}
/**
* Called by the framework before a remove user data operation takes place.
* Initiates a suitable UI request which will kick the worker thread.
*/
@Override
public void onReset() {
// reset the engine as if it was just created
super.onReset();
setRegistrationComplete(false);
mState = State.NOT_REGISTERED;
mRegistrationDetails = new RegistrationDetails();
mActivationCode = null;
onLoginStateChanged(false);
}
/**
* Set 'dummy' auth session for test purposes only.
*
* @param session 'dummy' session supplied to LoginEngine
*/
public static void setTestSession(AuthSessionHolder session) {
sActivatedSession = session;
}
}