2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 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.app;
20 import java.util.ArrayList;
21 import java.util.Date;
24 import android.app.Activity;
25 import android.app.AlertDialog;
26 import android.content.AsyncQueryHandler;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.Intent;
33 import android.content.res.Resources;
34 import android.database.ContentObserver;
35 import android.database.Cursor;
36 import android.database.CursorIndexOutOfBoundsException;
37 import android.database.DataSetObserver;
38 import android.database.CharArrayBuffer;
39 import android.graphics.Typeface;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.Message;
43 import android.os.RemoteException;
44 import android.provider.Im;
45 import android.text.TextUtils;
46 import android.text.style.StyleSpan;
47 import android.text.style.URLSpan;
48 import android.util.AttributeSet;
49 import android.util.Log;
50 import android.view.KeyEvent;
51 import android.view.LayoutInflater;
52 import android.view.View;
53 import android.view.ViewGroup;
54 import android.widget.AbsListView;
55 import android.widget.AdapterView;
56 import android.widget.ArrayAdapter;
57 import android.widget.Button;
58 import android.widget.CursorAdapter;
59 import android.widget.EditText;
60 import android.widget.ImageView;
61 import android.widget.LinearLayout;
62 import android.widget.ListView;
63 import android.widget.TextView;
64 import android.widget.AbsListView.OnScrollListener;
65 import android.widget.AdapterView.OnItemClickListener;
67 import com.android.im.IChatListener;
68 import com.android.im.IChatSession;
69 import com.android.im.IChatSessionListener;
70 import com.android.im.IChatSessionManager;
71 import com.android.im.IContactList;
72 import com.android.im.IContactListListener;
73 import com.android.im.IContactListManager;
74 import com.android.im.IImConnection;
75 import com.android.im.R;
76 import com.android.im.app.adapter.ChatListenerAdapter;
77 import com.android.im.app.adapter.ChatSessionListenerAdapter;
78 import com.android.im.engine.Contact;
79 import com.android.im.engine.ImConnection;
80 import com.android.im.engine.ImErrorInfo;
81 import com.android.im.plugin.BrandingResourceIDs;
83 public class ChatView extends LinearLayout {
84 // This projection and index are set for the query of active chats
85 static final String[] CHAT_PROJECTION = {
92 Im.Presence.PRESENCE_STATUS,
93 Im.Chats.LAST_UNREAD_MESSAGE,
95 static final int CONTACT_ID_COLUMN = 0;
96 static final int ACCOUNT_COLUMN = 1;
97 static final int PROVIDER_COLUMN = 2;
98 static final int USERNAME_COLUMN = 3;
99 static final int NICKNAME_COLUMN = 4;
100 static final int TYPE_COLUMN = 5;
101 static final int PRESENCE_STATUS_COLUMN = 6;
102 static final int LAST_UNREAD_MESSAGE_COLUMN = 7;
104 static final String[] INVITATION_PROJECT = {
106 Im.Invitation.PROVIDER,
107 Im.Invitation.SENDER,
109 static final int INVITATION_ID_COLUMN = 0;
110 static final int INVITATION_PROVIDER_COLUMN = 1;
111 static final int INVITATION_SENDER_COLUMN = 2;
113 static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
119 SimpleAlertHandler mHandler;
122 private ImageView mStatusIcon;
123 private TextView mTitle;
124 /*package*/ListView mHistory;
126 private Button mSendButton;
127 private View mStatusWarningView;
128 private ImageView mWarningIcon;
129 private TextView mWarningText;
131 private MessageAdapter mMessageAdapter;
132 private IChatSessionManager mChatSessionMgr;
133 private IChatSessionListener mChatSessionListener;
135 private IChatSession mChatSession;
136 private long mChatId;
143 private int mPresenceStatus;
145 private int mViewType;
147 private static final int VIEW_TYPE_CHAT = 1;
148 private static final int VIEW_TYPE_INVITATION = 2;
149 private static final int VIEW_TYPE_SUBSCRIPTION = 3;
151 private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000; // 1 minute
152 private static final int QUERY_TOKEN = 10;
154 // Async QueryHandler
155 private final class QueryHandler extends AsyncQueryHandler {
156 public QueryHandler(Context context) {
157 super(context.getContentResolver());
161 protected void onQueryComplete(int token, Object cookie, Cursor c) {
162 Cursor cursor = new DeltaCursor(c);
164 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
165 log("onQueryComplete: cursor.count=" + cursor.getCount());
168 mMessageAdapter.changeCursor(cursor);
171 private QueryHandler mQueryHandler;
173 private class RequeryCallback implements Runnable {
175 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
176 log("RequeryCallback");
181 private RequeryCallback mRequeryCallback = null;
183 private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
184 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
185 if (!(view instanceof MessageView)) {
188 URLSpan[] links = ((MessageView)view).getMessageLinks();
189 if (links.length == 0){
193 final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
194 for (URLSpan u : links) {
195 linkUrls.add(u.getURL());
197 ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen,
198 android.R.layout.select_dialog_item, linkUrls);
199 AlertDialog.Builder b = new AlertDialog.Builder(mScreen);
200 b.setTitle(R.string.select_link_title);
201 b.setCancelable(true);
202 b.setAdapter(a, new DialogInterface.OnClickListener() {
203 public void onClick(DialogInterface dialog, int which) {
204 Uri uri = Uri.parse(linkUrls.get(which));
205 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
206 intent.addCategory(Intent.CATEGORY_BROWSABLE);
207 mScreen.startActivity(intent);
210 b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
211 public void onClick(DialogInterface dialog, int which) {
219 private IChatListener mChatListener = new ChatListenerAdapter() {
221 public void onIncomingMessage(IChatSession ses,
222 com.android.im.engine.Message msg) {
227 public void onContactJoined(IChatSession ses, Contact contact) {
232 public void onContactLeft(IChatSession ses, Contact contact) {
237 public void onSendMessageError(IChatSession ses,
238 com.android.im.engine.Message msg, ImErrorInfo error) {
243 private Runnable mUpdateChatCallback = new Runnable() {
245 if (mCursor.requery() && mCursor.moveToFirst()) {
250 private IContactListListener mContactListListener = new IContactListListener.Stub () {
251 public void onAllContactListsLoaded() {
254 public void onContactChange(int type, IContactList list, Contact contact){
257 public void onContactError(int errorType, ImErrorInfo error,
258 String listName, Contact contact) {
261 public void onContactsPresenceUpdate(Contact[] contacts) {
262 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
263 log("onContactsPresenceUpdate()");
265 for (Contact c : contacts) {
266 if (c.getAddress().getFullName().equals(mUserName)) {
267 mHandler.post(mUpdateChatCallback);
275 static final void log(String msg) {
276 Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
279 public ChatView(Context context, AttributeSet attrs) {
280 super(context, attrs);
281 mScreen = (Activity) context;
282 mApp = ImApp.getApplication(mScreen);
283 mHandler = new ChatViewHandler();
286 void registerForConnEvents() {
287 mApp.registerForConnEvents(mHandler);
290 void unregisterForConnEvents() {
291 mApp.unregisterForConnEvents(mHandler);
295 protected void onFinishInflate() {
296 mStatusIcon = (ImageView) findViewById(R.id.statusIcon);
297 mTitle = (TextView) findViewById(R.id.title);
298 mHistory = (ListView) findViewById(R.id.history);
299 mEdtInput = (EditText) findViewById(R.id.edtInput);
300 mSendButton = (Button)findViewById(R.id.btnSend);
301 mHistory.setOnItemClickListener(mOnItemClickListener);
303 mStatusWarningView = findViewById(R.id.warning);
304 mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
305 mWarningText = (TextView)findViewById(R.id.warningText);
307 Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
308 Button declineInvitation= (Button)findViewById(R.id.btnDecline);
310 Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
311 Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);
313 acceptInvitation.setOnClickListener(new OnClickListener() {
314 public void onClick(View v) {
318 declineInvitation.setOnClickListener(new OnClickListener() {
319 public void onClick(View v) {
324 approveSubscription.setOnClickListener(new OnClickListener(){
325 public void onClick(View v) {
326 approveSubscription();
329 declineSubscription.setOnClickListener(new OnClickListener(){
330 public void onClick(View v) {
331 declineSubscription();
335 mEdtInput.setOnKeyListener(new OnKeyListener(){
336 public boolean onKey(View v, int keyCode, KeyEvent event) {
337 if (event.getAction() == KeyEvent.ACTION_DOWN) {
339 case KeyEvent.KEYCODE_DPAD_CENTER:
343 case KeyEvent.KEYCODE_ENTER:
344 if (event.isAltPressed()) {
345 mEdtInput.append("\n");
355 mSendButton.setOnClickListener(new OnClickListener() {
356 public void onClick(View v) {
362 public void onResume(){
363 if (mViewType == VIEW_TYPE_CHAT) {
364 Cursor cursor = getMessageCursor();
365 if (cursor == null) {
372 registerChatListener();
373 registerForConnEvents();
376 public void onPause(){
377 Cursor cursor = getMessageCursor();
378 if (cursor != null) {
382 if (mViewType == VIEW_TYPE_CHAT) {
385 unregisterChatListener();
386 unregisterForConnEvents();
387 unregisterChatSessionListener();
391 setViewType(VIEW_TYPE_CHAT);
393 long oldChatId = mChatId;
400 IImConnection conn = mApp.getConnection(mProviderId);
402 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
407 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
408 mHistory.setBackgroundDrawable(
409 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
411 if (mMarkup == null) {
412 mMarkup = new Markup(brandingRes);
415 if (mMessageAdapter == null) {
416 mMessageAdapter = new MessageAdapter(mScreen, null);
417 mHistory.setAdapter(mMessageAdapter);
420 // only change the message adapter when we switch to another chat
421 if (mChatId != oldChatId) {
423 mEdtInput.setText("");
429 private void updateContactInfo() {
430 mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
431 mProviderId = mCursor.getLong(PROVIDER_COLUMN);
432 mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
433 mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
434 mType = mCursor.getInt(TYPE_COLUMN);
435 mUserName = mCursor.getString(USERNAME_COLUMN);
436 mNickName = mCursor.getString(NICKNAME_COLUMN);
439 private void setTitle() {
440 if (mType == Im.Contacts.TYPE_GROUP) {
441 final String[] projection = {Im.GroupMembers.NICKNAME};
442 Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
443 ContentResolver cr = mScreen.getContentResolver();
444 Cursor c = cr.query(memberUri, projection, null, null, null);
445 StringBuilder buf = new StringBuilder();
447 while(c.moveToNext()) {
448 buf.append(c.getString(0));
455 mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
457 mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
461 private void setStatusIcon() {
462 if (mType == Im.Contacts.TYPE_GROUP) {
463 // hide the status icon for group chat.
464 mStatusIcon.setVisibility(GONE);
466 mStatusIcon.setVisibility(VISIBLE);
467 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
468 int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
469 mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
473 public void bindChat(long chatId) {
474 if (mCursor != null) {
475 mCursor.deactivate();
477 Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
478 mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
479 if (mCursor == null || !mCursor.moveToFirst()) {
480 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
481 log("Failed to query chat: " + chatId);
486 mChatSession = getChatSession(mCursor);
488 registerChatListener();
492 public void bindInvitation(long invitationId) {
493 Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
494 ContentResolver cr = mScreen.getContentResolver();
495 Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
496 if (cursor == null || !cursor.moveToFirst()) {
497 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
498 log("Failed to query invitation: " + invitationId);
502 setViewType(VIEW_TYPE_INVITATION);
504 mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
505 mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
506 String sender = cursor.getString(INVITATION_SENDER_COLUMN);
508 TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
509 mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
510 mTitle.setText(mContext.getString(R.string.chat_with, sender));
513 if (cursor != null) {
518 public void bindSubscription(long providerId, String from) {
519 mProviderId = providerId;
522 setViewType(VIEW_TYPE_SUBSCRIPTION);
524 TextView text = (TextView)findViewById(R.id.txtSubscription);
525 String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
526 text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
527 mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
530 void acceptInvitation() {
533 IImConnection conn = mApp.getConnection(mProviderId);
535 // register a chat session listener and wait for a group chat
536 // session to be created after we accept the invitation.
537 registerChatSessionListener();
538 conn.acceptInvitation(mInvitationId);
540 } catch (RemoteException e) {
541 mHandler.showServiceErrorAlert();
545 void declineInvitation() {
547 IImConnection conn = mApp.getConnection(mProviderId);
549 conn.rejectInvitation(mInvitationId);
552 } catch (RemoteException e) {
553 mHandler.showServiceErrorAlert();
557 void approveSubscription() {
558 IImConnection conn = mApp.getConnection(mProviderId);
560 IContactListManager manager = conn.getContactListManager();
561 manager.approveSubscription(mUserName);
562 } catch (RemoteException ex) {
563 mHandler.showServiceErrorAlert();
568 void declineSubscription() {
569 IImConnection conn = mApp.getConnection(mProviderId);
571 IContactListManager manager = conn.getContactListManager();
572 manager.declineSubscription(mUserName);
573 } catch (RemoteException ex) {
574 mHandler.showServiceErrorAlert();
579 private void setViewType(int type) {
581 if (type == VIEW_TYPE_CHAT) {
582 findViewById(R.id.invitationPanel).setVisibility(GONE);
583 findViewById(R.id.subscription).setVisibility(GONE);
584 setChatViewEnabled(true);
585 } else if (type == VIEW_TYPE_INVITATION) {
586 setChatViewEnabled(false);
587 findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
588 findViewById(R.id.btnAccept).requestFocus();
589 } else if (type == VIEW_TYPE_SUBSCRIPTION) {
590 setChatViewEnabled(false);
591 findViewById(R.id.subscription).setVisibility(VISIBLE);
592 findViewById(R.id.btnApproveSubscription).requestFocus();
596 private void setChatViewEnabled(boolean enabled) {
597 mEdtInput.setEnabled(enabled);
598 mSendButton.setEnabled(enabled);
600 mEdtInput.requestFocus();
602 mHistory.setAdapter(null);
606 private void markAsRead() {
607 ContentValues values = new ContentValues(1);
608 values.put(Im.Chats.LAST_UNREAD_MESSAGE, (String)null);
610 ContentResolver cr = mContext.getContentResolver();
611 Uri uri = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId);
612 cr.update(uri, values, null, null);
615 private void startQuery() {
616 if (mQueryHandler == null) {
617 mQueryHandler = new QueryHandler(mContext);
619 // Cancel any pending queries
620 mQueryHandler.cancelOperation(QUERY_TOKEN);
624 if (Im.Contacts.TYPE_GROUP == mType) {
625 uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
627 uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
630 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
631 log("queryCursor: uri=" + uri);
634 mQueryHandler.startQuery(QUERY_TOKEN, null,
637 null /* selection */,
638 null /* selection args */,
642 void scheduleRequery(long interval) {
643 if (mRequeryCallback == null) {
644 mRequeryCallback = new RequeryCallback();
646 mHandler.removeCallbacks(mRequeryCallback);
649 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
650 log("scheduleRequery");
652 mHandler.postDelayed(mRequeryCallback, interval);
655 void cancelRequery() {
656 if (mRequeryCallback != null) {
657 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
658 log("cancelRequery");
660 mHandler.removeCallbacks(mRequeryCallback);
661 mRequeryCallback = null;
665 void requeryCursor() {
666 if (mMessageAdapter.isScrolling()) {
667 mMessageAdapter.setNeedRequeryCursor(true);
670 // TODO: async query?
671 Cursor cursor = getMessageCursor();
672 if (cursor != null) {
677 private Cursor getMessageCursor() {
678 return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
681 public void insertSmiley(String smiley) {
682 mEdtInput.append(mMarkup.applyEmoticons(smiley));
685 public void closeChatSession() {
686 if (mChatSession != null) {
688 mChatSession.leave();
689 } catch (RemoteException e) {
690 mHandler.showServiceErrorAlert();
693 // the conversation is already closed, clear data in database
694 ContentResolver cr = mContext.getContentResolver();
695 cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
701 public void closeChatSessionIfInactive() {
702 if (mChatSession != null) {
704 mChatSession.leaveIfInactive();
705 } catch (RemoteException e) {
706 mHandler.showServiceErrorAlert();
711 public void viewProfile() {
712 Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
713 Intent intent = new Intent(Intent.ACTION_VIEW, data);
714 mScreen.startActivity(intent);
717 public void blockContact() {
718 // TODO: unify with codes in ContactListView
719 DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
720 public void onClick(DialogInterface dialog, int whichButton) {
722 IImConnection conn = mApp.getConnection(mProviderId);
723 IContactListManager manager = conn.getContactListManager();
724 manager.blockContact(mUserName);
726 } catch (RemoteException e) {
727 mHandler.showServiceErrorAlert();
732 Resources r = getResources();
734 // The positive button is deliberately set as no so that
735 // the no is the default value
736 new AlertDialog.Builder(mContext)
737 .setTitle(R.string.confirm)
738 .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
739 .setPositiveButton(R.string.no, null) // default button
740 .setNegativeButton(R.string.yes, confirmListener)
741 .setCancelable(false)
745 public long getProviderId() {
749 public long getAccountId() {
753 public String getUserName() {
757 public long getChatId () {
759 return mChatSession == null ? -1 : mChatSession.getId();
760 } catch (RemoteException e) {
761 mHandler.showServiceErrorAlert();
766 public IChatSession getCurrentChatSession() {
770 private IChatSessionManager getChatSessionManager(long providerId) {
771 if (mChatSessionMgr == null) {
772 IImConnection conn = mApp.getConnection(providerId);
775 mChatSessionMgr = conn.getChatSessionManager();
776 } catch (RemoteException e) {
777 mHandler.showServiceErrorAlert();
781 return mChatSessionMgr;
784 private IChatSession getChatSession(Cursor cursor) {
785 long providerId = cursor.getLong(PROVIDER_COLUMN);
786 String username = cursor.getString(USERNAME_COLUMN);
788 IChatSessionManager sessionMgr = getChatSessionManager(providerId);
789 if (sessionMgr != null) {
791 return sessionMgr.getChatSession(username);
792 } catch (RemoteException e) {
793 mHandler.showServiceErrorAlert();
799 boolean isGroupChat() {
800 return Im.Contacts.TYPE_GROUP == mType;
804 String msg = mEdtInput.getText().toString();
805 if (mChatSession != null && !TextUtils.isEmpty(msg.trim())) {
807 mChatSession.sendMessage(msg);
808 mEdtInput.setText("");
809 mEdtInput.requestFocus();
811 } catch (RemoteException e) {
812 mHandler.showServiceErrorAlert();
817 void registerChatListener() {
818 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
819 log("registerChatListener");
822 if (mChatSession != null) {
823 mChatSession.registerChatListener(mChatListener);
825 IImConnection conn = mApp.getConnection(mProviderId);
827 IContactListManager listMgr = conn.getContactListManager();
828 listMgr.registerContactListListener(mContactListListener);
830 mApp.dismissNotifications(mProviderId);
831 } catch (RemoteException e) {
832 Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
836 void unregisterChatListener() {
837 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
838 log("unregisterChatListener");
841 if (mChatSession != null) {
842 mChatSession.unregisterChatListener(mChatListener);
844 IImConnection conn = mApp.getConnection(mProviderId);
846 IContactListManager listMgr = conn.getContactListManager();
847 listMgr.unregisterContactListListener(mContactListListener);
849 } catch (RemoteException e) {
850 Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
854 void registerChatSessionListener() {
855 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
856 if (sessionMgr != null) {
857 mChatSessionListener = new ChatSessionListener();
859 sessionMgr.registerChatSessionListener(mChatSessionListener);
860 } catch (RemoteException e) {
861 mHandler.showServiceErrorAlert();
866 void unregisterChatSessionListener() {
867 if (mChatSessionListener != null) {
869 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
870 sessionMgr.unregisterChatSessionListener(mChatSessionListener);
871 // We unregister the listener when the chat session we are
872 // waiting for has been created or the activity is stopped.
873 // Clear the listener so that we won't unregister the listener
875 mChatSessionListener = null;
876 } catch (RemoteException e) {
877 mHandler.showServiceErrorAlert();
882 void updateWarningView() {
883 int visibility = View.GONE;
884 int iconVisibility = View.GONE;
885 String message = null;
889 IImConnection conn = mApp.getConnection(mProviderId);
890 isConnected = (conn == null) ? false
891 : conn.getState() != ImConnection.SUSPENDED;
892 } catch (RemoteException e) {
898 if (mType == Im.Contacts.TYPE_TEMPORARY) {
899 visibility = View.VISIBLE;
900 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
901 } else if (mPresenceStatus == Im.Presence.OFFLINE) {
902 visibility = View.VISIBLE;
903 message = mContext.getString(R.string.contact_offline_warning, mNickName);
906 visibility = View.VISIBLE;
907 iconVisibility = View.VISIBLE;
908 message = mContext.getString(R.string.disconnected_warning);
911 mStatusWarningView.setVisibility(visibility);
912 if (visibility == View.VISIBLE) {
913 mWarningIcon.setVisibility(iconVisibility);
914 mWarningText.setText(message);
918 private final class ChatViewHandler extends SimpleAlertHandler {
919 public ChatViewHandler() {
924 public void handleMessage(Message msg) {
925 long providerId = ((long)msg.arg1 << 32) | msg.arg2;
926 if (providerId != mProviderId) {
931 case ImApp.EVENT_CONNECTION_LOGGED_IN:
932 log("Connection resumed");
935 case ImApp.EVENT_CONNECTION_SUSPENDED:
936 log("Connection suspended");
941 super.handleMessage(msg);
945 class ChatSessionListener extends ChatSessionListenerAdapter {
947 public void onChatSessionCreated(IChatSession session) {
949 if (session.isGroupChatSession()) {
950 final long id = session.getId();
951 unregisterChatSessionListener();
952 mHandler.post(new Runnable() {
957 } catch (RemoteException e) {
958 mHandler.showServiceErrorAlert();
963 public static class DeltaCursor implements Cursor {
964 static final String DELTA_COLUMN_NAME = "delta";
966 private Cursor mCursor;
967 private String[] mColumnNames;
968 private int mDateColumn = -1;
969 private int mDeltaColumn = -1;
971 DeltaCursor(Cursor cursor) {
974 String[] columnNames = cursor.getColumnNames();
975 int len = columnNames.length;
977 mColumnNames = new String[len + 1];
979 for (int i = 0 ; i < len ; i++) {
980 mColumnNames[i] = columnNames[i];
981 if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
987 mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
989 //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
990 // mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
993 public int getCount() {
994 return mCursor.getCount();
997 public int getPosition() {
998 return mCursor.getPosition();
1001 public boolean move(int offset) {
1002 return mCursor.move(offset);
1005 public boolean moveToPosition(int position) {
1006 return mCursor.moveToPosition(position);
1009 public boolean moveToFirst() {
1010 return mCursor.moveToFirst();
1013 public boolean moveToLast() {
1014 return mCursor.moveToLast();
1017 public boolean moveToNext() {
1018 return mCursor.moveToNext();
1021 public boolean moveToPrevious() {
1022 return mCursor.moveToPrevious();
1025 public boolean isFirst() {
1026 return mCursor.isFirst();
1029 public boolean isLast() {
1030 return mCursor.isLast();
1033 public boolean isBeforeFirst() {
1034 return mCursor.isBeforeFirst();
1037 public boolean isAfterLast() {
1038 return mCursor.isAfterLast();
1041 public boolean deleteRow() {
1042 return mCursor.deleteRow();
1045 public int getColumnIndex(String columnName) {
1046 if (DELTA_COLUMN_NAME.equals(columnName)) {
1047 return mDeltaColumn;
1050 int columnIndex = mCursor.getColumnIndex(columnName);
1054 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1055 if (DELTA_COLUMN_NAME.equals(columnName)) {
1056 return mDeltaColumn;
1059 return mCursor.getColumnIndexOrThrow(columnName);
1062 public String getColumnName(int columnIndex) {
1063 if (columnIndex == mDeltaColumn) {
1064 return DELTA_COLUMN_NAME;
1067 return mCursor.getColumnName(columnIndex);
1070 public int getColumnCount() {
1071 return mCursor.getColumnCount() + 1;
1074 public boolean supportsUpdates() {
1075 return mCursor.supportsUpdates();
1078 public boolean hasUpdates() {
1079 return mCursor.hasUpdates();
1082 public boolean updateBlob(int columnIndex, byte[] value) {
1083 if (columnIndex == mDeltaColumn) {
1087 return mCursor.updateBlob(columnIndex, value);
1090 public boolean updateString(int columnIndex, String value) {
1091 if (columnIndex == mDeltaColumn) {
1095 return mCursor.updateString(columnIndex, value);
1098 public boolean updateShort(int columnIndex, short value) {
1099 if (columnIndex == mDeltaColumn) {
1103 return mCursor.updateShort(columnIndex, value);
1106 public boolean updateInt(int columnIndex, int value) {
1107 if (columnIndex == mDeltaColumn) {
1111 return mCursor.updateInt(columnIndex, value);
1114 public boolean updateLong(int columnIndex, long value) {
1115 if (columnIndex == mDeltaColumn) {
1119 return mCursor.updateLong(columnIndex, value);
1122 public boolean updateFloat(int columnIndex, float value) {
1123 if (columnIndex == mDeltaColumn) {
1127 return mCursor.updateFloat(columnIndex, value);
1130 public boolean updateDouble(int columnIndex, double value) {
1131 if (columnIndex == mDeltaColumn) {
1135 return mCursor.updateDouble(columnIndex, value);
1138 public boolean updateToNull(int columnIndex) {
1139 if (columnIndex == mDeltaColumn) {
1143 return mCursor.updateToNull(columnIndex);
1146 public boolean commitUpdates() {
1147 return mCursor.commitUpdates();
1150 public boolean commitUpdates(Map<? extends Long,
1151 ? extends Map<String,Object>> values) {
1152 return mCursor.commitUpdates(values);
1155 public void abortUpdates() {
1156 mCursor.abortUpdates();
1159 public void deactivate() {
1160 mCursor.deactivate();
1163 public boolean requery() {
1164 return mCursor.requery();
1167 public void close() {
1171 public boolean isClosed() {
1172 return mCursor.isClosed();
1175 public void registerContentObserver(ContentObserver observer) {
1176 mCursor.registerContentObserver(observer);
1179 public void unregisterContentObserver(ContentObserver observer) {
1180 mCursor.unregisterContentObserver(observer);
1183 public void registerDataSetObserver(DataSetObserver observer) {
1184 mCursor.registerDataSetObserver(observer);
1187 public void unregisterDataSetObserver(DataSetObserver observer) {
1188 mCursor.unregisterDataSetObserver(observer);
1191 public void setNotificationUri(ContentResolver cr, Uri uri) {
1192 mCursor.setNotificationUri(cr, uri);
1195 public boolean getWantsAllOnMoveCalls() {
1196 return mCursor.getWantsAllOnMoveCalls();
1199 public Bundle getExtras() {
1200 return mCursor.getExtras();
1203 public Bundle respond(Bundle extras) {
1204 return mCursor.respond(extras);
1207 public String[] getColumnNames() {
1208 return mColumnNames;
1211 private void checkPosition() {
1212 int pos = mCursor.getPosition();
1213 int count = mCursor.getCount();
1215 if (-1 == pos || count == pos) {
1216 throw new CursorIndexOutOfBoundsException(pos, count);
1220 public byte[] getBlob(int column) {
1223 if (column == mDeltaColumn) {
1227 return mCursor.getBlob(column);
1230 public String getString(int column) {
1233 if (column == mDeltaColumn) {
1234 long value = getDeltaValue();
1235 return Long.toString(value);
1238 return mCursor.getString(column);
1241 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1244 if (columnIndex == mDeltaColumn) {
1245 long value = getDeltaValue();
1246 String strValue = Long.toString(value);
1247 int len = strValue.length();
1248 char[] data = buffer.data;
1249 if (data == null || data.length < len) {
1250 buffer.data = strValue.toCharArray();
1252 strValue.getChars(0, len, data, 0);
1254 buffer.sizeCopied = strValue.length();
1256 mCursor.copyStringToBuffer(columnIndex, buffer);
1260 public short getShort(int column) {
1263 if (column == mDeltaColumn) {
1264 return (short)getDeltaValue();
1267 return mCursor.getShort(column);
1270 public int getInt(int column) {
1273 if (column == mDeltaColumn) {
1274 return (int)getDeltaValue();
1277 return mCursor.getInt(column);
1280 public long getLong(int column) {
1281 //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1284 if (column == mDeltaColumn) {
1285 return getDeltaValue();
1288 return mCursor.getLong(column);
1291 public float getFloat(int column) {
1294 if (column == mDeltaColumn) {
1295 return getDeltaValue();
1298 return mCursor.getFloat(column);
1301 public double getDouble(int column) {
1304 if (column == mDeltaColumn) {
1305 return getDeltaValue();
1308 return mCursor.getDouble(column);
1311 public boolean isNull(int column) {
1314 if (column == mDeltaColumn) {
1318 return mCursor.isNull(column);
1321 private long getDeltaValue() {
1322 int pos = mCursor.getPosition();
1323 //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1327 if (pos == getCount()-1) {
1328 t1 = mCursor.getLong(mDateColumn);
1329 t2 = System.currentTimeMillis();
1331 mCursor.moveToPosition(pos + 1);
1332 t2 = mCursor.getLong(mDateColumn);
1333 mCursor.moveToPosition(pos);
1334 t1 = mCursor.getLong(mDateColumn);
1341 private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1342 private int mScrollState;
1343 private boolean mNeedRequeryCursor;
1345 private int mContactColumn;
1346 private int mBodyColumn;
1347 private int mDateColumn;
1348 private int mTypeColumn;
1349 private int mErrCodeColumn;
1350 private int mDeltaColumn;
1351 private ChatBackgroundMaker mBgMaker;
1353 private LayoutInflater mInflater;
1355 public MessageAdapter(Activity context, Cursor c) {
1356 super(context, c, false);
1357 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1358 mBgMaker = new ChatBackgroundMaker(context);
1360 resolveColumnIndex(c);
1364 private void resolveColumnIndex(Cursor c) {
1365 mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
1366 mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
1367 mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
1368 mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
1369 mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
1370 mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1374 public void changeCursor(Cursor cursor) {
1375 super.changeCursor(cursor);
1376 if (cursor != null) {
1377 resolveColumnIndex(cursor);
1382 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1383 return mInflater.inflate(R.layout.new_message_item, parent, false);
1387 public void bindView(View view, Context context, Cursor cursor) {
1388 MessageView chatMsgView = (MessageView) view;
1390 int type = cursor.getInt(mTypeColumn);
1391 String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
1392 String body = cursor.getString(mBodyColumn);
1393 long delta = cursor.getLong(mDeltaColumn);
1394 boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1395 Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1398 case Im.MessageType.INCOMING:
1399 chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1402 case Im.MessageType.OUTGOING:
1403 case Im.MessageType.POSTPONED:
1404 int errCode = cursor.getInt(mErrCodeColumn);
1406 chatMsgView.bindErrorMessage(errCode);
1408 chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1413 chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1415 if (!isScrolling()) {
1416 mBgMaker.setBackground(chatMsgView, contact, type);
1419 // if showTimeStamp is false for the latest message, then set a timer to query the
1420 // cursor again in a minute, so we can update the last message timestamp if no new
1421 // message is received
1422 if (cursor.getPosition() == cursor.getCount()-1) {
1423 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1424 log("delta = " + delta + ", showTs=" + showTimeStamp);
1426 if (!showTimeStamp) {
1427 scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1434 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1435 int totalItemCount) {
1439 public void onScrollStateChanged(AbsListView view, int scrollState) {
1440 int oldState = mScrollState;
1441 mScrollState = scrollState;
1442 if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1443 if (mNeedRequeryCursor) {
1446 notifyDataSetChanged();
1451 boolean isScrolling() {
1452 return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1455 void setNeedRequeryCursor(boolean requeryCursor) {
1456 mNeedRequeryCursor = requeryCursor;