/* * 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); } }