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.Context;
30 import android.content.DialogInterface;
31 import android.content.Intent;
32 import android.content.res.Resources;
33 import android.content.res.Configuration;
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.Browser;
45 import android.provider.Im;
46 import android.text.Editable;
47 import android.text.TextUtils;
48 import android.text.TextWatcher;
49 import android.text.style.StyleSpan;
50 import android.text.style.URLSpan;
51 import android.util.AttributeSet;
52 import android.util.Log;
53 import android.view.KeyEvent;
54 import android.view.LayoutInflater;
55 import android.view.MotionEvent;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.view.inputmethod.InputMethodManager;
59 import android.widget.AbsListView;
60 import android.widget.AdapterView;
61 import android.widget.ArrayAdapter;
62 import android.widget.Button;
63 import android.widget.CursorAdapter;
64 import android.widget.EditText;
65 import android.widget.ImageView;
66 import android.widget.LinearLayout;
67 import android.widget.ListView;
68 import android.widget.TextView;
69 import android.widget.AbsListView.OnScrollListener;
70 import android.widget.AdapterView.OnItemClickListener;
72 import com.android.im.IChatListener;
73 import com.android.im.IChatSession;
74 import com.android.im.IChatSessionListener;
75 import com.android.im.IChatSessionManager;
76 import com.android.im.IContactList;
77 import com.android.im.IContactListListener;
78 import com.android.im.IContactListManager;
79 import com.android.im.IImConnection;
80 import com.android.im.R;
81 import com.android.im.app.adapter.ChatListenerAdapter;
82 import com.android.im.app.adapter.ChatSessionListenerAdapter;
83 import com.android.im.engine.Contact;
84 import com.android.im.engine.ImConnection;
85 import com.android.im.engine.ImErrorInfo;
86 import com.android.im.plugin.BrandingResourceIDs;
88 public class ChatView extends LinearLayout {
89 // This projection and index are set for the query of active chats
90 static final String[] CHAT_PROJECTION = {
97 Im.Presence.PRESENCE_STATUS,
98 Im.Chats.LAST_UNREAD_MESSAGE,
100 static final int CONTACT_ID_COLUMN = 0;
101 static final int ACCOUNT_COLUMN = 1;
102 static final int PROVIDER_COLUMN = 2;
103 static final int USERNAME_COLUMN = 3;
104 static final int NICKNAME_COLUMN = 4;
105 static final int TYPE_COLUMN = 5;
106 static final int PRESENCE_STATUS_COLUMN = 6;
107 static final int LAST_UNREAD_MESSAGE_COLUMN = 7;
109 static final String[] INVITATION_PROJECT = {
111 Im.Invitation.PROVIDER,
112 Im.Invitation.SENDER,
114 static final int INVITATION_ID_COLUMN = 0;
115 static final int INVITATION_PROVIDER_COLUMN = 1;
116 static final int INVITATION_SENDER_COLUMN = 2;
118 static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
124 SimpleAlertHandler mHandler;
127 private ImageView mStatusIcon;
128 private TextView mTitle;
129 /*package*/ListView mHistory;
131 private Button mSendButton;
132 private View mStatusWarningView;
133 private ImageView mWarningIcon;
134 private TextView mWarningText;
136 private MessageAdapter mMessageAdapter;
137 private IChatSessionManager mChatSessionMgr;
138 private IChatSessionListener mChatSessionListener;
140 private IChatSession mChatSession;
141 private long mChatId;
148 private int mPresenceStatus;
150 private int mViewType;
152 private static final int VIEW_TYPE_CHAT = 1;
153 private static final int VIEW_TYPE_INVITATION = 2;
154 private static final int VIEW_TYPE_SUBSCRIPTION = 3;
156 private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000; // 1 minute
157 private static final int QUERY_TOKEN = 10;
159 // Async QueryHandler
160 private final class QueryHandler extends AsyncQueryHandler {
161 public QueryHandler(Context context) {
162 super(context.getContentResolver());
166 protected void onQueryComplete(int token, Object cookie, Cursor c) {
167 Cursor cursor = new DeltaCursor(c);
169 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
170 log("onQueryComplete: cursor.count=" + cursor.getCount());
173 mMessageAdapter.changeCursor(cursor);
176 private QueryHandler mQueryHandler;
178 private class RequeryCallback implements Runnable {
180 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
181 log("RequeryCallback");
186 private RequeryCallback mRequeryCallback = null;
188 private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
189 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
190 if (!(view instanceof MessageView)) {
193 URLSpan[] links = ((MessageView)view).getMessageLinks();
194 if (links.length == 0){
198 final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
199 for (URLSpan u : links) {
200 linkUrls.add(u.getURL());
202 ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen,
203 android.R.layout.select_dialog_item, linkUrls);
204 AlertDialog.Builder b = new AlertDialog.Builder(mScreen);
205 b.setTitle(R.string.select_link_title);
206 b.setCancelable(true);
207 b.setAdapter(a, new DialogInterface.OnClickListener() {
208 public void onClick(DialogInterface dialog, int which) {
209 Uri uri = Uri.parse(linkUrls.get(which));
210 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
211 intent.putExtra(Browser.EXTRA_APPLICATION_ID, mScreen.getPackageName());
212 mScreen.startActivity(intent);
215 b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
216 public void onClick(DialogInterface dialog, int which) {
224 private IChatListener mChatListener = new ChatListenerAdapter() {
226 public void onIncomingMessage(IChatSession ses,
227 com.android.im.engine.Message msg) {
232 public void onContactJoined(IChatSession ses, Contact contact) {
237 public void onContactLeft(IChatSession ses, Contact contact) {
242 public void onSendMessageError(IChatSession ses,
243 com.android.im.engine.Message msg, ImErrorInfo error) {
248 private Runnable mUpdateChatCallback = new Runnable() {
250 if (mCursor.requery() && mCursor.moveToFirst()) {
255 private IContactListListener mContactListListener = new IContactListListener.Stub () {
256 public void onAllContactListsLoaded() {
259 public void onContactChange(int type, IContactList list, Contact contact){
262 public void onContactError(int errorType, ImErrorInfo error,
263 String listName, Contact contact) {
266 public void onContactsPresenceUpdate(Contact[] contacts) {
267 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
268 log("onContactsPresenceUpdate()");
270 for (Contact c : contacts) {
271 if (c.getAddress().getFullName().equals(mUserName)) {
272 mHandler.post(mUpdateChatCallback);
280 static final void log(String msg) {
281 Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
284 public ChatView(Context context, AttributeSet attrs) {
285 super(context, attrs);
286 mScreen = (Activity) context;
287 mApp = ImApp.getApplication(mScreen);
288 mHandler = new ChatViewHandler();
291 void registerForConnEvents() {
292 mApp.registerForConnEvents(mHandler);
295 void unregisterForConnEvents() {
296 mApp.unregisterForConnEvents(mHandler);
300 protected void onFinishInflate() {
301 mStatusIcon = (ImageView) findViewById(R.id.statusIcon);
302 mTitle = (TextView) findViewById(R.id.title);
303 mHistory = (ListView) findViewById(R.id.history);
304 mEdtInput = (EditText) findViewById(R.id.edtInput);
305 mSendButton = (Button)findViewById(R.id.btnSend);
306 mHistory.setOnItemClickListener(mOnItemClickListener);
308 mStatusWarningView = findViewById(R.id.warning);
309 mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
310 mWarningText = (TextView)findViewById(R.id.warningText);
312 Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
313 Button declineInvitation= (Button)findViewById(R.id.btnDecline);
315 Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
316 Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);
318 acceptInvitation.setOnClickListener(new OnClickListener() {
319 public void onClick(View v) {
323 declineInvitation.setOnClickListener(new OnClickListener() {
324 public void onClick(View v) {
329 approveSubscription.setOnClickListener(new OnClickListener(){
330 public void onClick(View v) {
331 approveSubscription();
334 declineSubscription.setOnClickListener(new OnClickListener(){
335 public void onClick(View v) {
336 declineSubscription();
340 mEdtInput.setOnKeyListener(new OnKeyListener(){
341 public boolean onKey(View v, int keyCode, KeyEvent event) {
342 if (event.getAction() == KeyEvent.ACTION_DOWN) {
344 case KeyEvent.KEYCODE_DPAD_CENTER:
348 case KeyEvent.KEYCODE_ENTER:
349 if (event.isAltPressed()) {
350 mEdtInput.append("\n");
359 mEdtInput.setOnEditorActionListener(new TextView.OnEditorActionListener() {
360 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
362 if (event.isAltPressed()) {
372 // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with
373 // the soft keyboard, we should remove this hack.
374 mEdtInput.addTextChangedListener(new TextWatcher() {
375 public void beforeTextChanged(CharSequence s, int start, int before, int after) {
378 public void onTextChanged(CharSequence s, int start, int before, int after) {
379 //log("TextWatcher: " + s);
380 userActionDetected();
383 public void afterTextChanged(Editable s) {
387 mSendButton.setOnClickListener(new OnClickListener() {
388 public void onClick(View v) {
394 public void onResume(){
395 if (mViewType == VIEW_TYPE_CHAT) {
396 Cursor cursor = getMessageCursor();
397 if (cursor == null) {
404 registerChatListener();
405 registerForConnEvents();
408 public void onPause(){
409 Cursor cursor = getMessageCursor();
410 if (cursor != null) {
414 if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) {
416 mChatSession.markAsRead();
417 } catch (RemoteException e) {
418 mHandler.showServiceErrorAlert();
421 unregisterChatListener();
422 unregisterForConnEvents();
423 unregisterChatSessionListener();
426 private void closeSoftKeyboard() {
427 InputMethodManager inputMethodManager =
428 (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE);
430 inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0);
434 setViewType(VIEW_TYPE_CHAT);
436 long oldChatId = mChatId;
443 IImConnection conn = mApp.getConnection(mProviderId);
445 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
450 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
451 mHistory.setBackgroundDrawable(
452 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
454 if (mMarkup == null) {
455 mMarkup = new Markup(brandingRes);
458 if (mMessageAdapter == null) {
459 mMessageAdapter = new MessageAdapter(mScreen, null);
460 mHistory.setAdapter(mMessageAdapter);
463 // only change the message adapter when we switch to another chat
464 if (mChatId != oldChatId) {
466 mEdtInput.setText("");
472 private void updateContactInfo() {
473 mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
474 mProviderId = mCursor.getLong(PROVIDER_COLUMN);
475 mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
476 mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
477 mType = mCursor.getInt(TYPE_COLUMN);
478 mUserName = mCursor.getString(USERNAME_COLUMN);
479 mNickName = mCursor.getString(NICKNAME_COLUMN);
482 private void setTitle() {
483 if (mType == Im.Contacts.TYPE_GROUP) {
484 final String[] projection = {Im.GroupMembers.NICKNAME};
485 Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
486 ContentResolver cr = mScreen.getContentResolver();
487 Cursor c = cr.query(memberUri, projection, null, null, null);
488 StringBuilder buf = new StringBuilder();
490 while(c.moveToNext()) {
491 buf.append(c.getString(0));
498 mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
500 mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
504 private void setStatusIcon() {
505 if (mType == Im.Contacts.TYPE_GROUP) {
506 // hide the status icon for group chat.
507 mStatusIcon.setVisibility(GONE);
509 mStatusIcon.setVisibility(VISIBLE);
510 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
511 int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
512 mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
516 public void bindChat(long chatId) {
517 if (mCursor != null) {
518 mCursor.deactivate();
520 Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
521 mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
522 if (mCursor == null || !mCursor.moveToFirst()) {
523 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
524 log("Failed to query chat: " + chatId);
529 mChatSession = getChatSession(mCursor);
531 registerChatListener();
535 public void bindInvitation(long invitationId) {
536 Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
537 ContentResolver cr = mScreen.getContentResolver();
538 Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
539 if (cursor == null || !cursor.moveToFirst()) {
540 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
541 log("Failed to query invitation: " + invitationId);
545 setViewType(VIEW_TYPE_INVITATION);
547 mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
548 mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
549 String sender = cursor.getString(INVITATION_SENDER_COLUMN);
551 TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
552 mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
553 mTitle.setText(mContext.getString(R.string.chat_with, sender));
556 if (cursor != null) {
561 public void bindSubscription(long providerId, String from) {
562 mProviderId = providerId;
565 setViewType(VIEW_TYPE_SUBSCRIPTION);
567 TextView text = (TextView)findViewById(R.id.txtSubscription);
568 String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
569 text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
570 mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
572 mApp.dismissChatNotification(providerId, from);
575 void acceptInvitation() {
578 IImConnection conn = mApp.getConnection(mProviderId);
580 // register a chat session listener and wait for a group chat
581 // session to be created after we accept the invitation.
582 registerChatSessionListener();
583 conn.acceptInvitation(mInvitationId);
585 } catch (RemoteException e) {
586 mHandler.showServiceErrorAlert();
590 void declineInvitation() {
592 IImConnection conn = mApp.getConnection(mProviderId);
594 conn.rejectInvitation(mInvitationId);
597 } catch (RemoteException e) {
598 mHandler.showServiceErrorAlert();
602 void approveSubscription() {
603 IImConnection conn = mApp.getConnection(mProviderId);
605 IContactListManager manager = conn.getContactListManager();
606 manager.approveSubscription(mUserName);
607 } catch (RemoteException ex) {
608 mHandler.showServiceErrorAlert();
613 void declineSubscription() {
614 IImConnection conn = mApp.getConnection(mProviderId);
616 IContactListManager manager = conn.getContactListManager();
617 manager.declineSubscription(mUserName);
618 } catch (RemoteException ex) {
619 mHandler.showServiceErrorAlert();
624 private void setViewType(int type) {
626 if (type == VIEW_TYPE_CHAT) {
627 findViewById(R.id.invitationPanel).setVisibility(GONE);
628 findViewById(R.id.subscription).setVisibility(GONE);
629 setChatViewEnabled(true);
630 } else if (type == VIEW_TYPE_INVITATION) {
631 setChatViewEnabled(false);
632 findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
633 findViewById(R.id.btnAccept).requestFocus();
634 } else if (type == VIEW_TYPE_SUBSCRIPTION) {
635 setChatViewEnabled(false);
636 findViewById(R.id.subscription).setVisibility(VISIBLE);
637 findViewById(R.id.btnApproveSubscription).requestFocus();
641 private void setChatViewEnabled(boolean enabled) {
642 mEdtInput.setEnabled(enabled);
643 mSendButton.setEnabled(enabled);
645 mEdtInput.requestFocus();
647 mHistory.setAdapter(null);
651 private void startQuery() {
652 if (mQueryHandler == null) {
653 mQueryHandler = new QueryHandler(mContext);
655 // Cancel any pending queries
656 mQueryHandler.cancelOperation(QUERY_TOKEN);
660 if (Im.Contacts.TYPE_GROUP == mType) {
661 uri = Im.Messages.getGroupChatContentUriByThreadId(mChatId);
663 uri = Im.Messages.getContentUriByThreadId(mChatId);
666 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
667 log("queryCursor: uri=" + uri);
670 mQueryHandler.startQuery(QUERY_TOKEN, null,
673 null /* selection */,
674 null /* selection args */,
678 void scheduleRequery(long interval) {
679 if (mRequeryCallback == null) {
680 mRequeryCallback = new RequeryCallback();
682 mHandler.removeCallbacks(mRequeryCallback);
685 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
686 log("scheduleRequery");
688 mHandler.postDelayed(mRequeryCallback, interval);
691 void cancelRequery() {
692 if (mRequeryCallback != null) {
693 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
694 log("cancelRequery");
696 mHandler.removeCallbacks(mRequeryCallback);
697 mRequeryCallback = null;
701 void requeryCursor() {
702 if (mMessageAdapter.isScrolling()) {
703 mMessageAdapter.setNeedRequeryCursor(true);
706 // TODO: async query?
707 Cursor cursor = getMessageCursor();
708 if (cursor != null) {
713 private Cursor getMessageCursor() {
714 return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
717 public void insertSmiley(String smiley) {
718 mEdtInput.append(mMarkup.applyEmoticons(smiley));
721 public void closeChatSession() {
722 if (mChatSession != null) {
724 mChatSession.leave();
725 } catch (RemoteException e) {
726 mHandler.showServiceErrorAlert();
729 // the conversation is already closed, clear data in database
730 ContentResolver cr = mContext.getContentResolver();
731 cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
737 public void closeChatSessionIfInactive() {
738 if (mChatSession != null) {
740 mChatSession.leaveIfInactive();
741 } catch (RemoteException e) {
742 mHandler.showServiceErrorAlert();
747 public void viewProfile() {
748 Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
749 Intent intent = new Intent(Intent.ACTION_VIEW, data);
750 mScreen.startActivity(intent);
753 public void blockContact() {
754 // TODO: unify with codes in ContactListView
755 DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
756 public void onClick(DialogInterface dialog, int whichButton) {
758 IImConnection conn = mApp.getConnection(mProviderId);
759 IContactListManager manager = conn.getContactListManager();
760 manager.blockContact(mUserName);
762 } catch (RemoteException e) {
763 mHandler.showServiceErrorAlert();
768 Resources r = getResources();
770 // The positive button is deliberately set as no so that
771 // the no is the default value
772 new AlertDialog.Builder(mContext)
773 .setTitle(R.string.confirm)
774 .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
775 .setPositiveButton(R.string.yes, confirmListener) // default button
776 .setNegativeButton(R.string.no, null)
777 .setCancelable(false)
781 public long getProviderId() {
785 public long getAccountId() {
789 public String getUserName() {
793 public long getChatId () {
795 return mChatSession == null ? -1 : mChatSession.getId();
796 } catch (RemoteException e) {
797 mHandler.showServiceErrorAlert();
802 public IChatSession getCurrentChatSession() {
806 private IChatSessionManager getChatSessionManager(long providerId) {
807 if (mChatSessionMgr == null) {
808 IImConnection conn = mApp.getConnection(providerId);
811 mChatSessionMgr = conn.getChatSessionManager();
812 } catch (RemoteException e) {
813 mHandler.showServiceErrorAlert();
817 return mChatSessionMgr;
820 private IChatSession getChatSession(Cursor cursor) {
821 long providerId = cursor.getLong(PROVIDER_COLUMN);
822 String username = cursor.getString(USERNAME_COLUMN);
824 IChatSessionManager sessionMgr = getChatSessionManager(providerId);
825 if (sessionMgr != null) {
827 return sessionMgr.getChatSession(username);
828 } catch (RemoteException e) {
829 mHandler.showServiceErrorAlert();
835 boolean isGroupChat() {
836 return Im.Contacts.TYPE_GROUP == mType;
840 String msg = mEdtInput.getText().toString();
842 if (TextUtils.isEmpty(msg.trim())) {
846 if (mChatSession != null) {
848 mChatSession.sendMessage(msg);
849 mEdtInput.setText("");
850 mEdtInput.requestFocus();
852 } catch (RemoteException e) {
853 mHandler.showServiceErrorAlert();
857 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
859 Configuration config = getResources().getConfiguration();
860 if (config.orientation == config.ORIENTATION_LANDSCAPE) {
865 void registerChatListener() {
866 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
867 log("registerChatListener");
870 if (mChatSession != null) {
871 mChatSession.registerChatListener(mChatListener);
873 IImConnection conn = mApp.getConnection(mProviderId);
875 IContactListManager listMgr = conn.getContactListManager();
876 listMgr.registerContactListListener(mContactListListener);
878 mApp.dismissChatNotification(mProviderId, mUserName);
879 } catch (RemoteException e) {
880 Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
884 void unregisterChatListener() {
885 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
886 log("unregisterChatListener");
889 if (mChatSession != null) {
890 mChatSession.unregisterChatListener(mChatListener);
892 IImConnection conn = mApp.getConnection(mProviderId);
894 IContactListManager listMgr = conn.getContactListManager();
895 listMgr.unregisterContactListListener(mContactListListener);
897 } catch (RemoteException e) {
898 Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
902 void registerChatSessionListener() {
903 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
904 if (sessionMgr != null) {
905 mChatSessionListener = new ChatSessionListener();
907 sessionMgr.registerChatSessionListener(mChatSessionListener);
908 } catch (RemoteException e) {
909 mHandler.showServiceErrorAlert();
914 void unregisterChatSessionListener() {
915 if (mChatSessionListener != null) {
917 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
918 sessionMgr.unregisterChatSessionListener(mChatSessionListener);
919 // We unregister the listener when the chat session we are
920 // waiting for has been created or the activity is stopped.
921 // Clear the listener so that we won't unregister the listener
923 mChatSessionListener = null;
924 } catch (RemoteException e) {
925 mHandler.showServiceErrorAlert();
930 void updateWarningView() {
931 int visibility = View.GONE;
932 int iconVisibility = View.GONE;
933 String message = null;
937 IImConnection conn = mApp.getConnection(mProviderId);
938 isConnected = (conn == null) ? false
939 : conn.getState() != ImConnection.SUSPENDED;
940 } catch (RemoteException e) {
946 if (mType == Im.Contacts.TYPE_TEMPORARY) {
947 visibility = View.VISIBLE;
948 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
949 } else if (mPresenceStatus == Im.Presence.OFFLINE) {
950 visibility = View.VISIBLE;
951 message = mContext.getString(R.string.contact_offline_warning, mNickName);
954 visibility = View.VISIBLE;
955 iconVisibility = View.VISIBLE;
956 message = mContext.getString(R.string.disconnected_warning);
959 mStatusWarningView.setVisibility(visibility);
960 if (visibility == View.VISIBLE) {
961 mWarningIcon.setVisibility(iconVisibility);
962 mWarningText.setText(message);
967 public boolean dispatchKeyEvent(KeyEvent event) {
968 userActionDetected();
969 return super.dispatchKeyEvent(event);
973 public boolean dispatchTouchEvent(MotionEvent ev) {
974 userActionDetected();
975 return super.dispatchTouchEvent(ev);
979 public boolean dispatchTrackballEvent(MotionEvent ev) {
980 userActionDetected();
981 return super.dispatchTrackballEvent(ev);
984 private void userActionDetected() {
985 if (mChatSession != null) {
987 mChatSession.markAsRead();
988 } catch (RemoteException e) {
989 mHandler.showServiceErrorAlert();
994 private final class ChatViewHandler extends SimpleAlertHandler {
995 public ChatViewHandler() {
1000 public void handleMessage(Message msg) {
1001 long providerId = ((long)msg.arg1 << 32) | msg.arg2;
1002 if (providerId != mProviderId) {
1007 case ImApp.EVENT_CONNECTION_LOGGED_IN:
1008 log("Connection resumed");
1009 updateWarningView();
1011 case ImApp.EVENT_CONNECTION_SUSPENDED:
1012 log("Connection suspended");
1013 updateWarningView();
1017 super.handleMessage(msg);
1021 class ChatSessionListener extends ChatSessionListenerAdapter {
1023 public void onChatSessionCreated(IChatSession session) {
1025 if (session.isGroupChatSession()) {
1026 final long id = session.getId();
1027 unregisterChatSessionListener();
1028 mHandler.post(new Runnable() {
1033 } catch (RemoteException e) {
1034 mHandler.showServiceErrorAlert();
1039 public static class DeltaCursor implements Cursor {
1040 static final String DELTA_COLUMN_NAME = "delta";
1042 private Cursor mInnerCursor;
1043 private String[] mColumnNames;
1044 private int mDateColumn = -1;
1045 private int mDeltaColumn = -1;
1047 DeltaCursor(Cursor cursor) {
1048 mInnerCursor = cursor;
1050 String[] columnNames = cursor.getColumnNames();
1051 int len = columnNames.length;
1053 mColumnNames = new String[len + 1];
1055 for (int i = 0 ; i < len ; i++) {
1056 mColumnNames[i] = columnNames[i];
1057 if (mColumnNames[i].equals(Im.Messages.DATE)) {
1063 mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
1065 //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
1066 // mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
1069 public int getCount() {
1070 return mInnerCursor.getCount();
1073 public int getPosition() {
1074 return mInnerCursor.getPosition();
1077 public boolean move(int offset) {
1078 return mInnerCursor.move(offset);
1081 public boolean moveToPosition(int position) {
1082 return mInnerCursor.moveToPosition(position);
1085 public boolean moveToFirst() {
1086 return mInnerCursor.moveToFirst();
1089 public boolean moveToLast() {
1090 return mInnerCursor.moveToLast();
1093 public boolean moveToNext() {
1094 return mInnerCursor.moveToNext();
1097 public boolean moveToPrevious() {
1098 return mInnerCursor.moveToPrevious();
1101 public boolean isFirst() {
1102 return mInnerCursor.isFirst();
1105 public boolean isLast() {
1106 return mInnerCursor.isLast();
1109 public boolean isBeforeFirst() {
1110 return mInnerCursor.isBeforeFirst();
1113 public boolean isAfterLast() {
1114 return mInnerCursor.isAfterLast();
1117 public boolean deleteRow() {
1118 return mInnerCursor.deleteRow();
1121 public int getColumnIndex(String columnName) {
1122 if (DELTA_COLUMN_NAME.equals(columnName)) {
1123 return mDeltaColumn;
1126 int columnIndex = mInnerCursor.getColumnIndex(columnName);
1130 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1131 if (DELTA_COLUMN_NAME.equals(columnName)) {
1132 return mDeltaColumn;
1135 return mInnerCursor.getColumnIndexOrThrow(columnName);
1138 public String getColumnName(int columnIndex) {
1139 if (columnIndex == mDeltaColumn) {
1140 return DELTA_COLUMN_NAME;
1143 return mInnerCursor.getColumnName(columnIndex);
1146 public int getColumnCount() {
1147 return mInnerCursor.getColumnCount() + 1;
1150 public boolean supportsUpdates() {
1151 return mInnerCursor.supportsUpdates();
1154 public boolean hasUpdates() {
1155 return mInnerCursor.hasUpdates();
1158 public boolean updateBlob(int columnIndex, byte[] value) {
1159 if (columnIndex == mDeltaColumn) {
1163 return mInnerCursor.updateBlob(columnIndex, value);
1166 public boolean updateString(int columnIndex, String value) {
1167 if (columnIndex == mDeltaColumn) {
1171 return mInnerCursor.updateString(columnIndex, value);
1174 public boolean updateShort(int columnIndex, short value) {
1175 if (columnIndex == mDeltaColumn) {
1179 return mInnerCursor.updateShort(columnIndex, value);
1182 public boolean updateInt(int columnIndex, int value) {
1183 if (columnIndex == mDeltaColumn) {
1187 return mInnerCursor.updateInt(columnIndex, value);
1190 public boolean updateLong(int columnIndex, long value) {
1191 if (columnIndex == mDeltaColumn) {
1195 return mInnerCursor.updateLong(columnIndex, value);
1198 public boolean updateFloat(int columnIndex, float value) {
1199 if (columnIndex == mDeltaColumn) {
1203 return mInnerCursor.updateFloat(columnIndex, value);
1206 public boolean updateDouble(int columnIndex, double value) {
1207 if (columnIndex == mDeltaColumn) {
1211 return mInnerCursor.updateDouble(columnIndex, value);
1214 public boolean updateToNull(int columnIndex) {
1215 if (columnIndex == mDeltaColumn) {
1219 return mInnerCursor.updateToNull(columnIndex);
1222 public boolean commitUpdates() {
1223 return mInnerCursor.commitUpdates();
1226 public boolean commitUpdates(Map<? extends Long,
1227 ? extends Map<String,Object>> values) {
1228 return mInnerCursor.commitUpdates(values);
1231 public void abortUpdates() {
1232 mInnerCursor.abortUpdates();
1235 public void deactivate() {
1236 mInnerCursor.deactivate();
1239 public boolean requery() {
1240 return mInnerCursor.requery();
1243 public void close() {
1244 mInnerCursor.close();
1247 public boolean isClosed() {
1248 return mInnerCursor.isClosed();
1251 public void registerContentObserver(ContentObserver observer) {
1252 mInnerCursor.registerContentObserver(observer);
1255 public void unregisterContentObserver(ContentObserver observer) {
1256 mInnerCursor.unregisterContentObserver(observer);
1259 public void registerDataSetObserver(DataSetObserver observer) {
1260 mInnerCursor.registerDataSetObserver(observer);
1263 public void unregisterDataSetObserver(DataSetObserver observer) {
1264 mInnerCursor.unregisterDataSetObserver(observer);
1267 public void setNotificationUri(ContentResolver cr, Uri uri) {
1268 mInnerCursor.setNotificationUri(cr, uri);
1271 public boolean getWantsAllOnMoveCalls() {
1272 return mInnerCursor.getWantsAllOnMoveCalls();
1275 public Bundle getExtras() {
1276 return mInnerCursor.getExtras();
1279 public Bundle respond(Bundle extras) {
1280 return mInnerCursor.respond(extras);
1283 public String[] getColumnNames() {
1284 return mColumnNames;
1287 private void checkPosition() {
1288 int pos = mInnerCursor.getPosition();
1289 int count = mInnerCursor.getCount();
1291 if (-1 == pos || count == pos) {
1292 throw new CursorIndexOutOfBoundsException(pos, count);
1296 public byte[] getBlob(int column) {
1299 if (column == mDeltaColumn) {
1303 return mInnerCursor.getBlob(column);
1306 public String getString(int column) {
1309 if (column == mDeltaColumn) {
1310 long value = getDeltaValue();
1311 return Long.toString(value);
1314 return mInnerCursor.getString(column);
1317 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1320 if (columnIndex == mDeltaColumn) {
1321 long value = getDeltaValue();
1322 String strValue = Long.toString(value);
1323 int len = strValue.length();
1324 char[] data = buffer.data;
1325 if (data == null || data.length < len) {
1326 buffer.data = strValue.toCharArray();
1328 strValue.getChars(0, len, data, 0);
1330 buffer.sizeCopied = strValue.length();
1332 mInnerCursor.copyStringToBuffer(columnIndex, buffer);
1336 public short getShort(int column) {
1339 if (column == mDeltaColumn) {
1340 return (short)getDeltaValue();
1343 return mInnerCursor.getShort(column);
1346 public int getInt(int column) {
1349 if (column == mDeltaColumn) {
1350 return (int)getDeltaValue();
1353 return mInnerCursor.getInt(column);
1356 public long getLong(int column) {
1357 //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1360 if (column == mDeltaColumn) {
1361 return getDeltaValue();
1364 return mInnerCursor.getLong(column);
1367 public float getFloat(int column) {
1370 if (column == mDeltaColumn) {
1371 return getDeltaValue();
1374 return mInnerCursor.getFloat(column);
1377 public double getDouble(int column) {
1380 if (column == mDeltaColumn) {
1381 return getDeltaValue();
1384 return mInnerCursor.getDouble(column);
1387 public boolean isNull(int column) {
1390 if (column == mDeltaColumn) {
1394 return mInnerCursor.isNull(column);
1397 private long getDeltaValue() {
1398 int pos = mInnerCursor.getPosition();
1399 //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1403 if (pos == getCount()-1) {
1404 t1 = mInnerCursor.getLong(mDateColumn);
1405 t2 = System.currentTimeMillis();
1407 mInnerCursor.moveToPosition(pos + 1);
1408 t2 = mInnerCursor.getLong(mDateColumn);
1409 mInnerCursor.moveToPosition(pos);
1410 t1 = mInnerCursor.getLong(mDateColumn);
1417 private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1418 private int mScrollState;
1419 private boolean mNeedRequeryCursor;
1421 private int mNicknameColumn;
1422 private int mBodyColumn;
1423 private int mDateColumn;
1424 private int mTypeColumn;
1425 private int mErrCodeColumn;
1426 private int mDeltaColumn;
1427 private ChatBackgroundMaker mBgMaker;
1429 private LayoutInflater mInflater;
1431 public MessageAdapter(Activity context, Cursor c) {
1432 super(context, c, false);
1433 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1434 mBgMaker = new ChatBackgroundMaker(context);
1436 resolveColumnIndex(c);
1440 private void resolveColumnIndex(Cursor c) {
1441 mNicknameColumn = c.getColumnIndexOrThrow(Im.Messages.NICKNAME);
1442 mBodyColumn = c.getColumnIndexOrThrow(Im.Messages.BODY);
1443 mDateColumn = c.getColumnIndexOrThrow(Im.Messages.DATE);
1444 mTypeColumn = c.getColumnIndexOrThrow(Im.Messages.TYPE);
1445 mErrCodeColumn = c.getColumnIndexOrThrow(Im.Messages.ERROR_CODE);
1446 mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1450 public void changeCursor(Cursor cursor) {
1451 super.changeCursor(cursor);
1452 if (cursor != null) {
1453 resolveColumnIndex(cursor);
1458 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1459 return mInflater.inflate(R.layout.new_message_item, parent, false);
1463 public void bindView(View view, Context context, Cursor cursor) {
1464 MessageView chatMsgView = (MessageView) view;
1466 int type = cursor.getInt(mTypeColumn);
1467 String contact = isGroupChat() ? cursor.getString(mNicknameColumn) : mNickName;
1468 String body = cursor.getString(mBodyColumn);
1469 long delta = cursor.getLong(mDeltaColumn);
1470 boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1471 Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1474 case Im.MessageType.INCOMING:
1475 chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1478 case Im.MessageType.OUTGOING:
1479 case Im.MessageType.POSTPONED:
1480 int errCode = cursor.getInt(mErrCodeColumn);
1482 chatMsgView.bindErrorMessage(errCode);
1484 chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1489 chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1491 if (!isScrolling()) {
1492 mBgMaker.setBackground(chatMsgView, contact, type);
1495 // if showTimeStamp is false for the latest message, then set a timer to query the
1496 // cursor again in a minute, so we can update the last message timestamp if no new
1497 // message is received
1498 if (cursor.getPosition() == cursor.getCount()-1) {
1499 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1500 log("delta = " + delta + ", showTs=" + showTimeStamp);
1502 if (!showTimeStamp) {
1503 scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1510 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1511 int totalItemCount) {
1515 public void onScrollStateChanged(AbsListView view, int scrollState) {
1516 int oldState = mScrollState;
1517 mScrollState = scrollState;
1519 if (mChatSession != null) {
1521 mChatSession.markAsRead();
1522 } catch (RemoteException e) {
1523 mHandler.showServiceErrorAlert();
1527 if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1528 if (mNeedRequeryCursor) {
1531 notifyDataSetChanged();
1536 boolean isScrolling() {
1537 return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1540 void setNeedRequeryCursor(boolean requeryCursor) {
1541 mNeedRequeryCursor = requeryCursor;