2 * Copyright (C) 2007-2008 Esmertec AG.
3 * Copyright (C) 2007-2008 The Android Open Source Project
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 package com.android.im.imps;
20 import java.util.ArrayList;
21 import java.util.HashMap;
24 import com.android.im.engine.ChatGroupManager;
25 import com.android.im.engine.ChatSessionManager;
26 import com.android.im.engine.Contact;
27 import com.android.im.engine.ContactListManager;
28 import com.android.im.engine.ImConnection;
29 import com.android.im.engine.ImErrorInfo;
30 import com.android.im.engine.ImException;
31 import com.android.im.engine.LoginInfo;
32 import com.android.im.engine.Presence;
33 import com.android.im.imps.ImpsConnectionConfig.CirMethod;
34 import com.android.im.imps.ImpsConnectionConfig.TransportType;
35 import com.android.im.imps.Primitive.TransactionMode;
38 * An implementation of ImConnection of Wireless Village IMPS protocol.
40 public class ImpsConnection extends ImConnection {
41 ImpsConnectionConfig mConfig;
43 DataChannel mDataChannel;
44 private CirChannel mCirChannel;
45 private PrimitiveDispatcherThread mDispatcherThread;
48 ImpsTransactionManager mTransactionManager;
49 private ImpsChatSessionManager mChatSessionManager;
50 private ImpsContactListManager mContactListManager;
51 private ImpsChatGroupManager mChatGroupManager;
52 private boolean mReestablishing;
55 * Constructs a new WVConnection with a WVConnectionConfig object.
57 * @param config the configuration.
58 * @throws ImException if there's an error in the configuration.
60 public ImpsConnection(ImpsConnectionConfig config) {
65 mTransactionManager = new ImpsTransactionManager(this);
66 mChatSessionManager = new ImpsChatSessionManager(this);
67 mContactListManager = new ImpsContactListManager(this);
68 mChatGroupManager = new ImpsChatGroupManager(this);
72 * Gets the configuration of this connection.
74 * @return the configuration.
76 ImpsConnectionConfig getConfig() {
80 synchronized void shutdownOnError(ImErrorInfo error) {
81 if(mState == DISCONNECTED) {
85 if (mCirChannel != null) {
86 mCirChannel.shutdown();
88 if (mDispatcherThread != null) {
89 mDispatcherThread.shutdown();
91 if (mDataChannel != null) {
92 mDataChannel.shutdown();
94 if (mContactListManager != null && !mReestablishing) {
95 mContactListManager.reset();
97 setState(mReestablishing ? SUSPENDED: DISCONNECTED, error);
98 mReestablishing = false;
102 shutdownOnError(null);
106 public int getCapability() {
107 return CAPABILITY_GROUP_CHAT | CAPABILITY_SESSION_REESTABLISHMENT;
111 public void loginAsync(LoginInfo loginInfo) {
112 if (!checkAndSetState(DISCONNECTED)) {
116 mSession = new ImpsSession(this, loginInfo);
117 } catch (ImException e) {
118 setState(DISCONNECTED, e.getImError());
125 public void reestablishSessionAsync(
126 HashMap<String, String> cookie) {
127 if (!checkAndSetState(SUSPENDED)) {
130 // If we can resume from the data channel, which means the
131 // session is still valid, we can just re-use the existing
132 // session and don't need to re-establish it.
133 if (mDataChannel.resume()) {
136 } catch(ImException e) {}
137 setState(LOGGED_IN, null);
139 // Failed to resume the data channel which means the
140 // session might have expired, we need to re-establish
141 // the session by signing in again.
142 mReestablishing = true;
144 mSession = new ImpsSession(this, cookie);
145 } catch (ImException e) {
146 setState(DISCONNECTED, e.getImError());
154 public void networkTypeChanged() {
155 if (mCirChannel != null) {
156 mCirChannel.reconnect();
160 private synchronized boolean checkAndSetState(int state) {
164 setState(LOGGING_IN, null);
168 private void doLogin() {
170 if (mConfig.useSmsAuth()) {
171 mDataChannel = new SmsDataChannel(this);
173 mDataChannel = createDataChannel();
175 mDataChannel.connect();
176 } catch (ImException e) {
177 ImErrorInfo error = e.getImError();
179 error = new ImErrorInfo(ImErrorInfo.UNKNOWN_LOGIN_ERROR,
182 shutdownOnError(error);
186 mDispatcherThread = new PrimitiveDispatcherThread(mDataChannel);
187 mDispatcherThread.start();
189 LoginTransaction login = new LoginTransaction();
190 login.startAuthenticate();
194 public HashMap<String, String> getSessionContext() {
195 if(mState != LOGGED_IN) {
198 return mSession.getContext();
202 class LoginTransaction extends MultiPhaseTransaction {
205 // We're not passing completion to ImpsAsyncTransaction. Instead
206 // we'll handle the notification in LoginTransaction.
207 super(mTransactionManager);
210 public void startAuthenticate() {
211 Primitive login = buildBasicLoginReq();
212 if (mConfig.use4wayLogin()) {
213 // first login request of 4 way login
214 String[] supportedDigestSchema = mConfig.getPasswordDigest().getSupportedDigestSchema();
215 for (String element : supportedDigestSchema) {
216 login.addElement(ImpsTags.DigestSchema, element);
220 login.addElement(ImpsTags.Password, mSession.getPassword());
226 public TransactionStatus processResponse(Primitive response) {
227 if (response.getElement(ImpsTags.SessionID) != null) {
228 // If server chooses authentication based on network, we might
229 // got the final Login-Response before the 2nd Login-Request.
230 String sessionId = response.getElementContents(ImpsTags.SessionID);
231 String keepAliveTime = response.getElementContents(ImpsTags.KeepAliveTime);
232 String capablityReqeust = response.getElementContents(ImpsTags.CapabilityRequest);
234 long keepAlive = ImpsUtils.parseLong(keepAliveTime,
235 mConfig.getDefaultKeepAliveInterval());
236 // make sure we always have time to send keep-alive requests.
237 // see buildBasicLoginReq().
239 mSession.setId(sessionId);
240 mSession.setKeepAliveTime(keepAlive);
241 mSession.setCapablityRequestRequired(ImpsUtils.isTrue(capablityReqeust));
244 return TransactionStatus.TRANSACTION_COMPLETED;
246 return sendSecondLogin(response);
251 public TransactionStatus processResponseError(ImpsErrorInfo error) {
252 if (error.getCode() == ImpsConstants.STATUS_UNAUTHORIZED
253 && error.getPrimitive() != null) {
254 if (mConfig.use4wayLogin()) {
255 // Not really an error. Send the 2nd Login-Request.
256 return sendSecondLogin(error.getPrimitive());
258 // We have already sent password in 2way login, while OZ's
259 // yahoo gateway server returns "401 - Further authorization
260 // required" instead of "409 - Invalid password" if the
261 // password only contains spaces.
262 shutdownOnError(new ImErrorInfo(409, "Invalid password"));
263 return TransactionStatus.TRANSACTION_COMPLETED;
265 } else if(error.getCode() == ImpsConstants.STATUS_COULD_NOT_RECOVER_SESSION) {
266 // The server could not recover the session, create a new
267 // session and try to login again.
268 LoginInfo loginInfo = mSession.getLoginInfo();
270 mSession = new ImpsSession(ImpsConnection.this, loginInfo);
271 } catch (ImException ignore) {
272 // This shouldn't happen since we have tried to login with
276 return TransactionStatus.TRANSACTION_COMPLETED;
278 shutdownOnError(error);
279 return TransactionStatus.TRANSACTION_COMPLETED;
283 private TransactionStatus sendSecondLogin(Primitive res) {
285 Primitive secondLogin = buildBasicLoginReq();
287 String nonce = res.getElementContents(ImpsTags.Nonce);
288 String digestSchema = res.getElementContents(ImpsTags.DigestSchema);
289 String digestBytes = mConfig.getPasswordDigest().digest(digestSchema, nonce,
290 mSession.getPassword());
292 secondLogin.addElement(ImpsTags.DigestBytes, digestBytes);
294 sendRequest(secondLogin);
295 return TransactionStatus.TRANSACTION_CONTINUE;
296 } catch (ImException e) {
298 shutdownOnError(new ImErrorInfo(ImErrorInfo.UNKNOWN_ERROR, e.toString()));
299 return TransactionStatus.TRANSACTION_COMPLETED;
303 private void onAuthenticated() {
304 // The user has chosen logout before the session established, just
305 // send the Logout-Request in this case.
306 if (mState == LOGGING_OUT) {
311 if (mConfig.useSmsAuth()
312 && mConfig.getDataChannelBinding() != TransportType.SMS) {
313 // SMS data channel was used if it's set to send authentication
314 // over SMS. Switch to the config data channel after authentication
317 DataChannel dataChannel = createDataChannel();
318 dataChannel.connect();
320 mDataChannel.shutdown();
321 mDataChannel = dataChannel;
322 mDispatcherThread.changeDataChannel(dataChannel);
323 } catch (ImException e) {
324 // This should not happen since only http data channel which
325 // does not do the real network connection in connect() is
332 if(mSession.isCapablityRequestRequired()) {
333 mSession.negotiateCapabilityAsync(new AsyncCompletion(){
334 public void onComplete() {
335 onCapabilityNegotiated();
338 public void onError(ImErrorInfo error) {
339 shutdownOnError(error);
343 onCapabilityNegotiated();
347 void onCapabilityNegotiated() {
348 mDataChannel.setServerMinPoll(mSession.getServerPollMin());
349 if(getConfig().getCirChannelBinding() != CirMethod.NONE) {
352 } catch (ImException e) {
353 shutdownOnError(new ImErrorInfo(
354 ImErrorInfo.UNSUPPORTED_CIR_CHANNEL, e.toString()));
359 mSession.negotiateServiceAsync(new AsyncCompletion(){
360 public void onComplete() {
361 onServiceNegotiated();
364 public void onError(ImErrorInfo error) {
365 shutdownOnError(error);
370 void onServiceNegotiated() {
371 mDataChannel.startKeepAlive(mSession.getKeepAliveTime());
373 retrieveUserPresenceAsync(new AsyncCompletion() {
374 public void onComplete() {
375 setState(LOGGED_IN, null);
376 if (mReestablishing) {
377 ImpsContactListManager listMgr= (ImpsContactListManager) getContactListManager();
378 listMgr.subscribeToAllListAsync();
379 mReestablishing = false;
383 public void onError(ImErrorInfo error) {
384 // Just continue. initUserPresenceAsync already made a
385 // default mUserPresence for us.
393 public void logoutAsync() {
394 setState(LOGGING_OUT, null);
395 // Shutdown the CIR channel first.
396 if(mCirChannel != null) {
397 mCirChannel.shutdown();
401 // Only send the Logout-Request if the session has been established.
402 if (mSession.getID() != null) {
407 void sendLogoutRequest() {
408 // We cannot shut down our connections in ImpsAsyncTransaction.onResponse()
409 // because at that time the logout transaction itself hasn't ended yet. So
410 // we have to do this in this completion object.
411 AsyncCompletion completion = new AsyncCompletion() {
412 public void onComplete() {
416 public void onError(ImErrorInfo error) {
417 // We simply ignore all errors when logging out.
418 // NowIMP responds a <Disconnect> instead of <Status> on logout request.
422 AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
424 Primitive logoutPrimitive = new Primitive(ImpsTags.Logout_Request);
425 tx.sendRequest(logoutPrimitive);
428 public ImpsSession getSession() {
433 public Contact getLoginUser() {
434 if(mSession == null){
437 Contact loginUser = mSession.getLoginUser();
438 loginUser.setPresence(getUserPresence());
443 public int[] getSupportedPresenceStatus() {
444 return mConfig.getPresenceMapping().getSupportedPresenceStatus();
447 public ImpsTransactionManager getTransactionManager() {
448 return mTransactionManager;
452 public ChatSessionManager getChatSessionManager() {
453 return mChatSessionManager;
457 public ContactListManager getContactListManager() {
458 return mContactListManager;
462 public ChatGroupManager getChatGroupManager() {
463 return mChatGroupManager;
467 * Sends a specific primitive to the server. It will return immediately
468 * after the primitive has been put to the sending queue.
470 * @param primitive the packet to send.
472 void sendPrimitive(Primitive primitive) {
473 mDataChannel.sendPrimitive(primitive);
477 * Sends a PollingRequest to the server.
479 void sendPollingRequest() {
480 Primitive pollingRequest = new Primitive(ImpsTags.Polling_Request);
481 pollingRequest.setSession(getSession().getID());
482 mDataChannel.sendPrimitive(pollingRequest);
485 private DataChannel createDataChannel() throws ImException {
486 TransportType dataChannelBinding = mConfig.getDataChannelBinding();
487 if (dataChannelBinding == TransportType.HTTP) {
488 return new HttpDataChannel(this);
489 } else if (dataChannelBinding == TransportType.SMS) {
490 return new SmsDataChannel(this);
492 throw new ImException("Unsupported data channel binding");
496 void setupCIRChannel() throws ImException {
497 if(mConfig.getDataChannelBinding() == TransportType.SMS) {
498 // No CIR channel is needed, do nothing.
501 CirMethod cirMethod = mSession.getCurrentCirMethod();
502 if (cirMethod == null) {
503 cirMethod = mConfig.getCirChannelBinding();
505 if (!mSession.getSupportedCirMethods().contains(cirMethod)) {
506 // Sever don't support the CIR method
507 cirMethod = CirMethod.SHTTP;
509 mSession.setCurrentCirMethod(cirMethod);
512 if (cirMethod == CirMethod.SHTTP) {
513 mCirChannel = new HttpCirChannel(this, mDataChannel);
514 } else if (cirMethod == CirMethod.STCP) {
515 mCirChannel = new TcpCirChannel(this);
516 } else if (cirMethod == CirMethod.SSMS) {
517 mCirChannel = new SmsCirChannel(this);
518 } else if (cirMethod == CirMethod.NONE) {
521 throw new ImException(ImErrorInfo.UNSUPPORTED_CIR_CHANNEL,
522 "Unsupported CIR channel binding");
525 if(mCirChannel != null) {
526 mCirChannel.connect();
530 private class PrimitiveDispatcherThread extends Thread {
531 private boolean stopped;
532 private DataChannel mChannel;
534 public PrimitiveDispatcherThread(DataChannel channel)
536 super("ImpsPrimitiveDispatcher");
540 public void changeDataChannel(DataChannel channel) {
547 Primitive primitive = null;
550 primitive = mChannel.receivePrimitive();
551 } catch (InterruptedException e) {
558 if (primitive != null) {
560 processIncomingPrimitive(primitive);
561 } catch (Throwable t) {
562 // We don't know what is going to happen in the various
564 ImpsLog.logError("ImpsDispatcher: uncaught Throwable", t);
577 * Handles the primitive received from the server.
579 * @param primitive the received primitive.
581 void processIncomingPrimitive(Primitive primitive) {
582 // if CIR is 'F', the CIR channel is not available. Re-establish it.
583 if (primitive.getCir() != null && ImpsUtils.isFalse(primitive.getCir())) {
584 if(mCirChannel != null) {
585 mCirChannel.shutdown();
589 } catch (ImException e) {
594 if (primitive.getPoll() != null && ImpsUtils.isTrue(primitive.getPoll())) {
595 sendPollingRequest();
598 if (primitive.getType().equals(ImpsTags.Disconnect)) {
599 if (mState != LOGGING_OUT) {
600 ImErrorInfo error = ImpsUtils.checkResultError(primitive);
601 shutdownOnError(error);
606 if (primitive.getTransactionMode() == TransactionMode.Response) {
607 ImpsErrorInfo error = ImpsUtils.checkResultError(primitive);
609 int code = error.getCode();
610 if (code == ImpsErrorInfo.SESSION_EXPIRED
611 || code == ImpsErrorInfo.FORCED_LOGOUT
612 || code == ImpsErrorInfo.INVALID_SESSION) {
613 shutdownOnError(error);
619 // According to the IMPS spec, only VersionDiscoveryResponse which
620 // are not supported now doesn't have a transaction ID.
621 if (primitive.getTransactionID() != null) {
622 mTransactionManager.notifyIncomingPrimitive(primitive);
627 protected void doUpdateUserPresenceAsync(Presence presence) {
628 ArrayList<PrimitiveElement> presenceSubList = ImpsPresenceUtils.buildUpdatePresenceElems(
629 mUserPresence, presence, mConfig.getPresenceMapping());
630 Primitive request = buildUpdatePresenceReq(presenceSubList);
631 // Need to make a copy because the presence passed in may change
632 // before the transaction finishes.
633 final Presence newPresence = new Presence(presence);
635 AsyncTransaction tx = new AsyncTransaction(mTransactionManager) {
638 public void onResponseOk(Primitive response) {
639 savePresenceChange(newPresence);
640 notifyUserPresenceUpdated();
644 public void onResponseError(ImpsErrorInfo error) {
645 notifyUpdateUserPresenceError(error);
648 tx.sendRequest(request);
651 void savePresenceChange(Presence newPresence) {
652 mUserPresence.setStatusText(newPresence.getStatusText());
653 mUserPresence.setStatus(newPresence.getStatus());
654 mUserPresence.setAvatar(newPresence.getAvatarData(), newPresence.getAvatarType());
655 // no need to update extended info because it's always read only.
658 void retrieveUserPresenceAsync(final AsyncCompletion completion) {
659 Primitive request = new Primitive(ImpsTags.GetPresence_Request);
661 request.addElement(this.getSession().getLoginUserAddress().toPrimitiveElement());
662 AsyncTransaction tx = new AsyncTransaction(mTransactionManager){
665 public void onResponseOk(Primitive response) {
666 PrimitiveElement presence = response.getElement(ImpsTags.Presence);
667 PrimitiveElement presenceSubList = presence.getChild(ImpsTags.PresenceSubList);
668 mUserPresence = ImpsPresenceUtils.extractPresence(presenceSubList,
669 mConfig.getPresenceMapping());
670 // XXX: workaround for the OZ IMPS GTalk server that
671 // returns an initial 'F' OnlineStatus. Set the online
672 // status to available in this case.
673 if(mUserPresence.getStatus() == Presence.OFFLINE) {
674 mUserPresence.setStatus(Presence.AVAILABLE);
676 compareAndUpdateClientInfo();
680 public void onResponseError(ImpsErrorInfo error) {
681 mUserPresence = new Presence(Presence.AVAILABLE, "", null,
682 null, Presence.CLIENT_TYPE_MOBILE, ImpsUtils.getClientInfo());
683 completion.onError(error);
686 private void compareAndUpdateClientInfo() {
687 if (!ImpsUtils.getClientInfo().equals(mUserPresence.getExtendedInfo())) {
688 updateClientInfoAsync(completion);
691 // no need to update our client info to the server again
692 completion.onComplete();
696 tx.sendRequest(request);
699 void updateClientInfoAsync(AsyncCompletion completion) {
700 Primitive updatePresenceRequest = buildUpdatePresenceReq(buildClientInfoElem());
702 AsyncTransaction tx = new SimpleAsyncTransaction(mTransactionManager,
704 tx.sendRequest(updatePresenceRequest);
707 private Primitive buildUpdatePresenceReq(PrimitiveElement presence) {
708 ArrayList<PrimitiveElement> presences = new ArrayList<PrimitiveElement>();
710 presences.add(presence);
712 return buildUpdatePresenceReq(presences);
715 private Primitive buildUpdatePresenceReq(ArrayList<PrimitiveElement> presences) {
716 Primitive updatePresenceRequest = new Primitive(ImpsTags.UpdatePresence_Request);
718 PrimitiveElement presenceSubList = updatePresenceRequest
719 .addElement(ImpsTags.PresenceSubList);
720 presenceSubList.setAttribute(ImpsTags.XMLNS, mConfig.getPresenceNs());
722 for (PrimitiveElement presence : presences) {
723 presenceSubList.addChild(presence);
726 return updatePresenceRequest;
729 private PrimitiveElement buildClientInfoElem() {
730 PrimitiveElement clientInfo = new PrimitiveElement(ImpsTags.ClientInfo);
731 clientInfo.addChild(ImpsTags.Qualifier, true);
733 Map<String, String> map = ImpsUtils.getClientInfo();
734 for (Map.Entry<String, String> item : map.entrySet()) {
735 clientInfo.addChild(item.getKey(), item.getValue());
741 Primitive buildBasicLoginReq() {
742 Primitive login = new Primitive(ImpsTags.Login_Request);
743 login.addElement(ImpsTags.UserID, mSession.getUserName());
744 PrimitiveElement clientId = login.addElement(ImpsTags.ClientID);
745 clientId.addChild(ImpsTags.URL, mConfig.getClientId());
746 if (mConfig.getMsisdn() != null) {
747 clientId.addChild(ImpsTags.MSISDN, mConfig.getMsisdn());
749 // we request for a bigger TimeToLive value than our default keep
750 // alive interval to make sure we always have time to send the keep
752 login.addElement(ImpsTags.TimeToLive,
753 Integer.toString(mConfig.getDefaultKeepAliveInterval() + 5));
754 login.addElement(ImpsTags.SessionCookie, mSession.getCookie());
759 synchronized public void suspend() {
760 setState(SUSPENDING, null);
762 if (mCirChannel != null) {
763 mCirChannel.shutdown();
766 if (mDataChannel != null) {
767 mDataChannel.suspend();
770 setState(SUSPENDED, null);