blob: 8f84cdbe824025f9d35a61d6f72601b23acf7bec [file] [log] [blame]
/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.im.imps;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.android.im.engine.ChatGroupManager;
import com.android.im.engine.ChatSessionManager;
import com.android.im.engine.Contact;
import com.android.im.engine.ContactListManager;
import com.android.im.engine.ImConnection;
import com.android.im.engine.ImErrorInfo;
import com.android.im.engine.ImException;
import com.android.im.engine.LoginInfo;
import com.android.im.engine.Presence;
import com.android.im.imps.ImpsConnectionConfig.CirMethod;
import com.android.im.imps.ImpsConnectionConfig.TransportType;
import com.android.im.imps.Primitive.TransactionMode;
/**
* An implementation of ImConnection of Wireless Village IMPS protocol.
*/
public class ImpsConnection extends ImConnection {
ImpsConnectionConfig mConfig;
DataChannel mDataChannel;
private CirChannel mCirChannel;
private PrimitiveDispatcherThread mDispatcherThread;
ImpsSession mSession;
ImpsTransactionManager mTransactionManager;
private ImpsChatSessionManager mChatSessionManager;
private ImpsContactListManager mContactListManager;
private ImpsChatGroupManager mChatGroupManager;
private boolean mReestablishing;
/**
* Constructs a new WVConnection with a WVConnectionConfig object.
*
* @param config the configuration.
* @throws ImException if there's an error in the configuration.
*/
public ImpsConnection(ImpsConnectionConfig config) {
super();
mConfig = config;
mTransactionManager = new ImpsTransactionManager(this);
mChatSessionManager = new ImpsChatSessionManager(this);
mContactListManager = new ImpsContactListManager(this);
mChatGroupManager = new ImpsChatGroupManager(this);
}
/**
* Gets the configuration of this connection.
*
* @return the configuration.
*/
ImpsConnectionConfig getConfig() {
return mConfig;
}
synchronized void shutdownOnError(ImErrorInfo error) {
if(mState == DISCONNECTED) {
return;
}
if (mCirChannel != null) {
mCirChannel.shutdown();
}
if (mDispatcherThread != null) {
mDispatcherThread.shutdown();
}
if (mDataChannel != null) {
mDataChannel.shutdown();
}
if (mContactListManager != null && !mReestablishing) {
mContactListManager.reset();
}
setState(mReestablishing ? SUSPENDED: DISCONNECTED, error);
mReestablishing = false;
}
void shutdown(){
shutdownOnError(null);
}
@Override
public int getCapability() {
return CAPABILITY_GROUP_CHAT | CAPABILITY_SESSION_REESTABLISHMENT;
}
@Override
public void loginAsync(LoginInfo loginInfo) {
if (!checkAndSetState(DISCONNECTED)) {
return;
}
try {
mSession = new ImpsSession(this, loginInfo);
} catch (ImException e) {
setState(DISCONNECTED, e.getImError());
return;
}
doLogin();
}
@Override
public void reestablishSessionAsync(
HashMap<String, String> cookie) {
if (!checkAndSetState(SUSPENDED)) {
return;
}
// If we can resume from the data channel, which means the
// session is still valid, we can just re-use the existing
// session and don't need to re-establish it.
if (mDataChannel.resume()) {
try {
setupCIRChannel();
} catch(ImException e) {}
setState(LOGGED_IN, null);
} else {
// Failed to resume the data channel which means the
// session might have expired, we need to re-establish
// the session by signing in again.
mReestablishing = true;
try {
mSession = new ImpsSession(this, cookie);
} catch (ImException e) {
setState(DISCONNECTED, e.getImError());
return;
}
doLogin();
}
}
@Override
public void networkTypeChanged() {
if (mCirChannel != null) {
mCirChannel.reconnect();
}
}
private synchronized boolean checkAndSetState(int state) {
if(mState != state){
return false;
}
setState(LOGGING_IN, null);
return true;
}
private void doLogin() {
try {
if (mConfig.useSmsAuth()) {
mDataChannel = new SmsDataChannel(this);
} else {
mDataChannel = createDataChannel();
}
mDataChannel.connect();
} catch (ImException e) {
ImErrorInfo error = e.getImError();
if(error == null){
error = new ImErrorInfo(ImErrorInfo.UNKNOWN_LOGIN_ERROR,
e.getMessage());
}
shutdownOnError(error);
return;
}
mDispatcherThread = new PrimitiveDispatcherThread(mDataChannel);
mDispatcherThread.start();
LoginTransaction login = new LoginTransaction();
login.startAuthenticate();
}
@Override
public HashMap<String, String> getSessionContext() {
if(mState != LOGGED_IN) {
return null;
} else {
return mSession.getContext();
}
}
class LoginTransaction extends MultiPhaseTransaction {
LoginTransaction() {
// We're not passing completion to ImpsAsyncTransaction. Instead
// we'll handle the notification in LoginTransaction.
super(mTransactionManager);
}
public void startAuthenticate() {
Primitive login = buildBasicLoginReq();
if (mConfig.use4wayLogin()) {
// first login request of 4 way login
String[] supportedDigestSchema = mConfig.getPasswordDigest().getSupportedDigestSchema();
for (String element : supportedDigestSchema) {
login.addElement(ImpsTags.DigestSchema, element);
}
} else {
// 2 way login
login.addElement(ImpsTags.Password, mSession.getPassword());
}
sendRequest(login);
}
@Override
public TransactionStatus processResponse(Primitive response) {
if (response.getElement(ImpsTags.SessionID) != null) {
// If server chooses authentication based on network, we might
// got the final Login-Response before the 2nd Login-Request.
String sessionId = response.getElementContents(ImpsTags.SessionID);
String keepAliveTime = response.getElementContents(ImpsTags.KeepAliveTime);
String capablityReqeust = response.getElementContents(ImpsTags.CapabilityRequest);
long keepAlive = ImpsUtils.parseLong(keepAliveTime,
mConfig.getDefaultKeepAliveInterval());
// make sure we always have time to send keep-alive requests.
// see buildBasicLoginReq().
keepAlive -= 5;
mSession.setId(sessionId);
mSession.setKeepAliveTime(keepAlive);
mSession.setCapablityRequestRequired(ImpsUtils.isTrue(capablityReqeust));
onAuthenticated();
return TransactionStatus.TRANSACTION_COMPLETED;
} else {
return sendSecondLogin(response);
}
}
@Override
public TransactionStatus processResponseError(ImpsErrorInfo error) {
if (error.getCode() == ImpsConstants.STATUS_UNAUTHORIZED
&& error.getPrimitive() != null) {
if (mConfig.use4wayLogin()) {
// Not really an error. Send the 2nd Login-Request.
return sendSecondLogin(error.getPrimitive());
} else {
// We have already sent password in 2way login, while OZ's
// yahoo gateway server returns "401 - Further authorization
// required" instead of "409 - Invalid password" if the
// password only contains spaces.
shutdownOnError(new ImErrorInfo(409, "Invalid password"));
return TransactionStatus.TRANSACTION_COMPLETED;
}
} else if(error.getCode() == ImpsConstants.STATUS_COULD_NOT_RECOVER_SESSION) {
// The server could not recover the session, create a new
// session and try to login again.
LoginInfo loginInfo = mSession.getLoginInfo();
try {
mSession = new ImpsSession(ImpsConnection.this, loginInfo);
} catch (ImException ignore) {
// This shouldn't happen since we have tried to login with
// the loginInfo
}
startAuthenticate();
return TransactionStatus.TRANSACTION_COMPLETED;
} else {
shutdownOnError(error);
return TransactionStatus.TRANSACTION_COMPLETED;
}
}
private TransactionStatus sendSecondLogin(Primitive res) {
try {
Primitive secondLogin = buildBasicLoginReq();
String nonce = res.getElementContents(ImpsTags.Nonce);
String digestSchema = res.getElementContents(ImpsTags.DigestSchema);
String digestBytes = mConfig.getPasswordDigest().digest(digestSchema, nonce,
mSession.getPassword());
secondLogin.addElement(ImpsTags.DigestBytes, digestBytes);
sendRequest(secondLogin);
return TransactionStatus.TRANSACTION_CONTINUE;
} catch (ImException e) {
ImpsLog.logError(e);
shutdownOnError(new ImErrorInfo(ImErrorInfo.UNKNOWN_ERROR, e.toString()));
return TransactionStatus.TRANSACTION_COMPLETED;
}
}
private void onAuthenticated() {
// The user has chosen logout before the session established, just
// send the Logout-Request in this case.
if (mState == LOGGING_OUT) {
sendLogoutRequest();
return;
}
if (mConfig.useSmsAuth()
&& mConfig.getDataChannelBinding() != TransportType.SMS) {
// SMS data channel was used if it's set to send authentication
// over SMS. Switch to the config data channel after authentication
// completed.
try {
DataChannel dataChannel = createDataChannel();
dataChannel.connect();
mDataChannel.shutdown();
mDataChannel = dataChannel;
mDispatcherThread.changeDataChannel(dataChannel);
} catch (ImException e) {
// This should not happen since only http data channel which
// does not do the real network connection in connect() is
// valid here now.
logoutAsync();
return;
}
}
if(mSession.isCapablityRequestRequired()) {
mSession.negotiateCapabilityAsync(new AsyncCompletion(){
public void onComplete() {
onCapabilityNegotiated();
}
public void onError(ImErrorInfo error) {
shutdownOnError(error);
}
});
} else {
onCapabilityNegotiated();
}
}
void onCapabilityNegotiated() {
mDataChannel.setServerMinPoll(mSession.getServerPollMin());
if(getConfig().getCirChannelBinding() != CirMethod.NONE) {
try {
setupCIRChannel();
} catch (ImException e) {
shutdownOnError(new ImErrorInfo(
ImErrorInfo.UNSUPPORTED_CIR_CHANNEL, e.toString()));
return;
}
}
mSession.negotiateServiceAsync(new AsyncCompletion(){
public void onComplete() {
onServiceNegotiated();
}
public void onError(ImErrorInfo error) {
shutdownOnError(error);
}
});
}
void onServiceNegotiated() {
mDataChannel.startKeepAlive(mSession.getKeepAliveTime());
retrieveUserPresenceAsync(new AsyncCompletion() {
public void onComplete() {
setState(LOGGED_IN, null);
if (mReestablishing) {
ImpsContactListManager listMgr= (ImpsContactListManager) getContactListManager();
listMgr.subscribeToAllListAsync();
mReestablishing = false;
}
}
public void onError(ImErrorInfo error) {
// Just continue. initUserPresenceAsync already made a
// default mUserPresence for us.
onComplete();
}
});
}
}
@Override
public void logoutAsync() {
setState(LOGGING_OUT, null);
// Shutdown the CIR channel first.
if(mCirChannel != null) {
mCirChannel.shutdown();
mCirChannel = null;
}
// Only send the Logout-Request if the session has been established.
if (mSession.getID() != null) {
sendLogoutRequest();
}
}
void sendLogoutRequest() {
// We cannot shut down our connections in ImpsAsyncTransaction.onResponse()
// because at that time the logout transaction itself hasn't ended yet. So
// we have to do this in this completion object.
AsyncCompletion completion = new AsyncCompletion() {
public void onComplete() {
shutdown();
}
public void onError(ImErrorInfo error) {
// We simply ignore all errors when logging out.
// NowIMP responds a <Disconnect> instead of <Status> on logout request.
shutdown();
}
};
AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
completion);
Primitive logoutPrimitive = new Primitive(ImpsTags.Logout_Request);
tx.sendRequest(logoutPrimitive);
}
public ImpsSession getSession() {
return mSession;
}
@Override
public Contact getLoginUser() {
if(mSession == null){
return null;
}
Contact loginUser = mSession.getLoginUser();
loginUser.setPresence(getUserPresence());
return loginUser;
}
@Override
public int[] getSupportedPresenceStatus() {
return mConfig.getPresenceMapping().getSupportedPresenceStatus();
}
public ImpsTransactionManager getTransactionManager() {
return mTransactionManager;
}
@Override
public ChatSessionManager getChatSessionManager() {
return mChatSessionManager;
}
@Override
public ContactListManager getContactListManager() {
return mContactListManager;
}
@Override
public ChatGroupManager getChatGroupManager() {
return mChatGroupManager;
}
/**
* Sends a specific primitive to the server. It will return immediately
* after the primitive has been put to the sending queue.
*
* @param primitive the packet to send.
*/
void sendPrimitive(Primitive primitive) {
mDataChannel.sendPrimitive(primitive);
}
/**
* Sends a PollingRequest to the server.
*/
void sendPollingRequest() {
Primitive pollingRequest = new Primitive(ImpsTags.Polling_Request);
pollingRequest.setSession(getSession().getID());
mDataChannel.sendPrimitive(pollingRequest);
}
private DataChannel createDataChannel() throws ImException {
TransportType dataChannelBinding = mConfig.getDataChannelBinding();
if (dataChannelBinding == TransportType.HTTP) {
return new HttpDataChannel(this);
} else if (dataChannelBinding == TransportType.SMS) {
return new SmsDataChannel(this);
} else {
throw new ImException("Unsupported data channel binding");
}
}
void setupCIRChannel() throws ImException {
if(mConfig.getDataChannelBinding() == TransportType.SMS) {
// No CIR channel is needed, do nothing.
return;
}
CirMethod cirMethod = mSession.getCurrentCirMethod();
if (cirMethod == null) {
cirMethod = mConfig.getCirChannelBinding();
if (!mSession.getSupportedCirMethods().contains(cirMethod)) {
// Sever don't support the CIR method
cirMethod = CirMethod.SHTTP;
}
mSession.setCurrentCirMethod(cirMethod);
}
if (cirMethod == CirMethod.SHTTP) {
mCirChannel = new HttpCirChannel(this, mDataChannel);
} else if (cirMethod == CirMethod.STCP) {
mCirChannel = new TcpCirChannel(this);
} else if (cirMethod == CirMethod.SSMS) {
mCirChannel = new SmsCirChannel(this);
} else if (cirMethod == CirMethod.NONE) {
//Do nothing
} else {
throw new ImException(ImErrorInfo.UNSUPPORTED_CIR_CHANNEL,
"Unsupported CIR channel binding");
}
if(mCirChannel != null) {
mCirChannel.connect();
}
}
private class PrimitiveDispatcherThread extends Thread {
private boolean stopped;
private DataChannel mChannel;
public PrimitiveDispatcherThread(DataChannel channel)
{
super("ImpsPrimitiveDispatcher");
mChannel = channel;
}
public void changeDataChannel(DataChannel channel) {
mChannel = channel;
interrupt();
}
@Override
public void run() {
Primitive primitive = null;
while (!stopped) {
try {
primitive = mChannel.receivePrimitive();
} catch (InterruptedException e) {
if (stopped) {
break;
}
primitive = null;
}
if (primitive != null) {
try {
processIncomingPrimitive(primitive);
} catch (Throwable t) {
// We don't know what is going to happen in the various
// listeners.
ImpsLog.logError("ImpsDispatcher: uncaught Throwable", t);
}
}
}
}
void shutdown() {
stopped = true;
interrupt();
}
}
/**
* Handles the primitive received from the server.
*
* @param primitive the received primitive.
*/
void processIncomingPrimitive(Primitive primitive) {
// if CIR is 'F', the CIR channel is not available. Re-establish it.
if (primitive.getCir() != null && ImpsUtils.isFalse(primitive.getCir())) {
if(mCirChannel != null) {
mCirChannel.shutdown();
}
try {
setupCIRChannel();
} catch (ImException e) {
e.printStackTrace();
}
}
if (primitive.getPoll() != null && ImpsUtils.isTrue(primitive.getPoll())) {
sendPollingRequest();
}
if (primitive.getType().equals(ImpsTags.Disconnect)) {
if (mState != LOGGING_OUT) {
ImErrorInfo error = ImpsUtils.checkResultError(primitive);
shutdownOnError(error);
return;
}
}
if (primitive.getTransactionMode() == TransactionMode.Response) {
ImpsErrorInfo error = ImpsUtils.checkResultError(primitive);
if (error != null) {
int code = error.getCode();
if (code == ImpsErrorInfo.SESSION_EXPIRED
|| code == ImpsErrorInfo.FORCED_LOGOUT
|| code == ImpsErrorInfo.INVALID_SESSION) {
shutdownOnError(error);
return;
}
}
}
// According to the IMPS spec, only VersionDiscoveryResponse which
// are not supported now doesn't have a transaction ID.
if (primitive.getTransactionID() != null) {
mTransactionManager.notifyIncomingPrimitive(primitive);
}
}
@Override
protected void doUpdateUserPresenceAsync(Presence presence) {
ArrayList<PrimitiveElement> presenceSubList = ImpsPresenceUtils.buildUpdatePresenceElems(
mUserPresence, presence, mConfig.getPresenceMapping());
Primitive request = buildUpdatePresenceReq(presenceSubList);
// Need to make a copy because the presence passed in may change
// before the transaction finishes.
final Presence newPresence = new Presence(presence);
AsyncTransaction tx = new AsyncTransaction(mTransactionManager) {
@Override
public void onResponseOk(Primitive response) {
savePresenceChange(newPresence);
notifyUserPresenceUpdated();
}
@Override
public void onResponseError(ImpsErrorInfo error) {
notifyUpdateUserPresenceError(error);
}
};
tx.sendRequest(request);
}
void savePresenceChange(Presence newPresence) {
mUserPresence.setStatusText(newPresence.getStatusText());
mUserPresence.setStatus(newPresence.getStatus());
mUserPresence.setAvatar(newPresence.getAvatarData(), newPresence.getAvatarType());
// no need to update extended info because it's always read only.
}
void retrieveUserPresenceAsync(final AsyncCompletion completion) {
Primitive request = new Primitive(ImpsTags.GetPresence_Request);
request.addElement(this.getSession().getLoginUserAddress().toPrimitiveElement());
AsyncTransaction tx = new AsyncTransaction(mTransactionManager){
@Override
public void onResponseOk(Primitive response) {
PrimitiveElement presence = response.getElement(ImpsTags.Presence);
PrimitiveElement presenceSubList = presence.getChild(ImpsTags.PresenceSubList);
mUserPresence = ImpsPresenceUtils.extractPresence(presenceSubList,
mConfig.getPresenceMapping());
// XXX: workaround for the OZ IMPS GTalk server that
// returns an initial 'F' OnlineStatus. Set the online
// status to available in this case.
if(mUserPresence.getStatus() == Presence.OFFLINE) {
mUserPresence.setStatus(Presence.AVAILABLE);
}
compareAndUpdateClientInfo();
}
@Override
public void onResponseError(ImpsErrorInfo error) {
mUserPresence = new Presence(Presence.AVAILABLE, "", null,
null, Presence.CLIENT_TYPE_MOBILE, ImpsUtils.getClientInfo());
completion.onError(error);
}
private void compareAndUpdateClientInfo() {
if (!ImpsUtils.getClientInfo().equals(mUserPresence.getExtendedInfo())) {
updateClientInfoAsync(completion);
return;
}
// no need to update our client info to the server again
completion.onComplete();
}
};
tx.sendRequest(request);
}
void updateClientInfoAsync(AsyncCompletion completion) {
Primitive updatePresenceRequest = buildUpdatePresenceReq(buildClientInfoElem());
AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
completion);
tx.sendRequest(updatePresenceRequest);
}
private Primitive buildUpdatePresenceReq(PrimitiveElement presence) {
ArrayList<PrimitiveElement> presences = new ArrayList<PrimitiveElement>();
presences.add(presence);
return buildUpdatePresenceReq(presences);
}
private Primitive buildUpdatePresenceReq(ArrayList<PrimitiveElement> presences) {
Primitive updatePresenceRequest = new Primitive(ImpsTags.UpdatePresence_Request);
PrimitiveElement presenceSubList = updatePresenceRequest
.addElement(ImpsTags.PresenceSubList);
presenceSubList.setAttribute(ImpsTags.XMLNS, mConfig.getPresenceNs());
for (PrimitiveElement presence : presences) {
presenceSubList.addChild(presence);
}
return updatePresenceRequest;
}
private PrimitiveElement buildClientInfoElem() {
PrimitiveElement clientInfo = new PrimitiveElement(ImpsTags.ClientInfo);
clientInfo.addChild(ImpsTags.Qualifier, true);
Map<String, String> map = ImpsUtils.getClientInfo();
for (Map.Entry<String, String> item : map.entrySet()) {
clientInfo.addChild(item.getKey(), item.getValue());
}
return clientInfo;
}
Primitive buildBasicLoginReq() {
Primitive login = new Primitive(ImpsTags.Login_Request);
login.addElement(ImpsTags.UserID, mSession.getUserName());
PrimitiveElement clientId = login.addElement(ImpsTags.ClientID);
clientId.addChild(ImpsTags.URL, mConfig.getClientId());
if (mConfig.getMsisdn() != null) {
clientId.addChild(ImpsTags.MSISDN, mConfig.getMsisdn());
}
// we request for a bigger TimeToLive value than our default keep
// alive interval to make sure we always have time to send the keep
// alive requests.
login.addElement(ImpsTags.TimeToLive,
Integer.toString(mConfig.getDefaultKeepAliveInterval() + 5));
login.addElement(ImpsTags.SessionCookie, mSession.getCookie());
return login;
}
@Override
synchronized public void suspend() {
setState(SUSPENDING, null);
if (mCirChannel != null) {
mCirChannel.shutdown();
}
if (mDataChannel != null) {
mDataChannel.suspend();
}
setState(SUSPENDED, null);
}
}