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.content.res.Configuration;
35 import android.database.ContentObserver;
36 import android.database.Cursor;
37 import android.database.CursorIndexOutOfBoundsException;
38 import android.database.DataSetObserver;
39 import android.database.CharArrayBuffer;
40 import android.graphics.Typeface;
41 import android.net.Uri;
42 import android.os.Bundle;
43 import android.os.Message;
44 import android.os.RemoteException;
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.addCategory(Intent.CATEGORY_BROWSABLE);
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");
360 // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with
361 // the soft keyboard, we should remove this hack.
362 mEdtInput.addTextChangedListener(new TextWatcher() {
363 public void beforeTextChanged(CharSequence s, int start, int before, int after) {
366 public void onTextChanged(CharSequence s, int start, int before, int after) {
367 //log("TextWatcher: " + s);
368 userActionDetected();
371 public void afterTextChanged(Editable s) {
375 mSendButton.setOnClickListener(new OnClickListener() {
376 public void onClick(View v) {
382 public void onResume(){
383 if (mViewType == VIEW_TYPE_CHAT) {
384 Cursor cursor = getMessageCursor();
385 if (cursor == null) {
392 registerChatListener();
393 registerForConnEvents();
396 public void onPause(){
397 Cursor cursor = getMessageCursor();
398 if (cursor != null) {
402 if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) {
404 mChatSession.markAsRead();
405 } catch (RemoteException e) {
406 mHandler.showServiceErrorAlert();
409 unregisterChatListener();
410 unregisterForConnEvents();
411 unregisterChatSessionListener();
414 private void handleEnterKey() {
415 Configuration config = getResources().getConfiguration();
416 if (config.orientation == config.ORIENTATION_LANDSCAPE) {
417 // in the landscape mode, we'll send the message if the user is using a physical
418 // keyboard. However, on the soft keyboard, we'll close the keyboard and put the
419 // focus on the Send button, in order to prevent accidental sending the message.
420 if (config.hardKeyboardHidden == config.HARDKEYBOARDHIDDEN_NO) {
424 mSendButton.requestFocus();
427 // in the portrait mode, the user would always be using the soft keyboard, so pressing
428 // the Enter key would close the keyboard and puts the focus on the Send button.
430 mSendButton.requestFocus();
434 private void closeSoftKeyboard() {
435 InputMethodManager inputMethodManager =
436 (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE);
438 inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0);
442 setViewType(VIEW_TYPE_CHAT);
444 long oldChatId = mChatId;
451 IImConnection conn = mApp.getConnection(mProviderId);
453 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
458 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
459 mHistory.setBackgroundDrawable(
460 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
462 if (mMarkup == null) {
463 mMarkup = new Markup(brandingRes);
466 if (mMessageAdapter == null) {
467 mMessageAdapter = new MessageAdapter(mScreen, null);
468 mHistory.setAdapter(mMessageAdapter);
471 // only change the message adapter when we switch to another chat
472 if (mChatId != oldChatId) {
474 mEdtInput.setText("");
480 private void updateContactInfo() {
481 mChatId = mCursor.getLong(CONTACT_ID_COLUMN);
482 mProviderId = mCursor.getLong(PROVIDER_COLUMN);
483 mAccountId = mCursor.getLong(ACCOUNT_COLUMN);
484 mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN);
485 mType = mCursor.getInt(TYPE_COLUMN);
486 mUserName = mCursor.getString(USERNAME_COLUMN);
487 mNickName = mCursor.getString(NICKNAME_COLUMN);
490 private void setTitle() {
491 if (mType == Im.Contacts.TYPE_GROUP) {
492 final String[] projection = {Im.GroupMembers.NICKNAME};
493 Uri memberUri = ContentUris.withAppendedId(Im.GroupMembers.CONTENT_URI, mChatId);
494 ContentResolver cr = mScreen.getContentResolver();
495 Cursor c = cr.query(memberUri, projection, null, null, null);
496 StringBuilder buf = new StringBuilder();
498 while(c.moveToNext()) {
499 buf.append(c.getString(0));
506 mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
508 mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
512 private void setStatusIcon() {
513 if (mType == Im.Contacts.TYPE_GROUP) {
514 // hide the status icon for group chat.
515 mStatusIcon.setVisibility(GONE);
517 mStatusIcon.setVisibility(VISIBLE);
518 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
519 int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
520 mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
524 public void bindChat(long chatId) {
525 if (mCursor != null) {
526 mCursor.deactivate();
528 Uri contactUri = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, chatId);
529 mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null);
530 if (mCursor == null || !mCursor.moveToFirst()) {
531 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
532 log("Failed to query chat: " + chatId);
537 mChatSession = getChatSession(mCursor);
539 registerChatListener();
543 public void bindInvitation(long invitationId) {
544 Uri uri = ContentUris.withAppendedId(Im.Invitation.CONTENT_URI, invitationId);
545 ContentResolver cr = mScreen.getContentResolver();
546 Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null);
547 if (cursor == null || !cursor.moveToFirst()) {
548 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
549 log("Failed to query invitation: " + invitationId);
553 setViewType(VIEW_TYPE_INVITATION);
555 mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
556 mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
557 String sender = cursor.getString(INVITATION_SENDER_COLUMN);
559 TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation);
560 mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender));
561 mTitle.setText(mContext.getString(R.string.chat_with, sender));
564 if (cursor != null) {
569 public void bindSubscription(long providerId, String from) {
570 mProviderId = providerId;
573 setViewType(VIEW_TYPE_SUBSCRIPTION);
575 TextView text = (TextView)findViewById(R.id.txtSubscription);
576 String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from);
577 text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr));
578 mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr));
580 mApp.dismissChatNotification(providerId, from);
583 void acceptInvitation() {
586 IImConnection conn = mApp.getConnection(mProviderId);
588 // register a chat session listener and wait for a group chat
589 // session to be created after we accept the invitation.
590 registerChatSessionListener();
591 conn.acceptInvitation(mInvitationId);
593 } catch (RemoteException e) {
594 mHandler.showServiceErrorAlert();
598 void declineInvitation() {
600 IImConnection conn = mApp.getConnection(mProviderId);
602 conn.rejectInvitation(mInvitationId);
605 } catch (RemoteException e) {
606 mHandler.showServiceErrorAlert();
610 void approveSubscription() {
611 IImConnection conn = mApp.getConnection(mProviderId);
613 IContactListManager manager = conn.getContactListManager();
614 manager.approveSubscription(mUserName);
615 } catch (RemoteException ex) {
616 mHandler.showServiceErrorAlert();
621 void declineSubscription() {
622 IImConnection conn = mApp.getConnection(mProviderId);
624 IContactListManager manager = conn.getContactListManager();
625 manager.declineSubscription(mUserName);
626 } catch (RemoteException ex) {
627 mHandler.showServiceErrorAlert();
632 private void setViewType(int type) {
634 if (type == VIEW_TYPE_CHAT) {
635 findViewById(R.id.invitationPanel).setVisibility(GONE);
636 findViewById(R.id.subscription).setVisibility(GONE);
637 setChatViewEnabled(true);
638 } else if (type == VIEW_TYPE_INVITATION) {
639 setChatViewEnabled(false);
640 findViewById(R.id.invitationPanel).setVisibility(VISIBLE);
641 findViewById(R.id.btnAccept).requestFocus();
642 } else if (type == VIEW_TYPE_SUBSCRIPTION) {
643 setChatViewEnabled(false);
644 findViewById(R.id.subscription).setVisibility(VISIBLE);
645 findViewById(R.id.btnApproveSubscription).requestFocus();
649 private void setChatViewEnabled(boolean enabled) {
650 mEdtInput.setEnabled(enabled);
651 mSendButton.setEnabled(enabled);
653 mEdtInput.requestFocus();
655 mHistory.setAdapter(null);
659 private void startQuery() {
660 if (mQueryHandler == null) {
661 mQueryHandler = new QueryHandler(mContext);
663 // Cancel any pending queries
664 mQueryHandler.cancelOperation(QUERY_TOKEN);
668 if (Im.Contacts.TYPE_GROUP == mType) {
669 uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
671 uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
674 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
675 log("queryCursor: uri=" + uri);
678 mQueryHandler.startQuery(QUERY_TOKEN, null,
681 null /* selection */,
682 null /* selection args */,
686 void scheduleRequery(long interval) {
687 if (mRequeryCallback == null) {
688 mRequeryCallback = new RequeryCallback();
690 mHandler.removeCallbacks(mRequeryCallback);
693 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
694 log("scheduleRequery");
696 mHandler.postDelayed(mRequeryCallback, interval);
699 void cancelRequery() {
700 if (mRequeryCallback != null) {
701 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
702 log("cancelRequery");
704 mHandler.removeCallbacks(mRequeryCallback);
705 mRequeryCallback = null;
709 void requeryCursor() {
710 if (mMessageAdapter.isScrolling()) {
711 mMessageAdapter.setNeedRequeryCursor(true);
714 // TODO: async query?
715 Cursor cursor = getMessageCursor();
716 if (cursor != null) {
721 private Cursor getMessageCursor() {
722 return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
725 public void insertSmiley(String smiley) {
726 mEdtInput.append(mMarkup.applyEmoticons(smiley));
729 public void closeChatSession() {
730 if (mChatSession != null) {
732 mChatSession.leave();
733 } catch (RemoteException e) {
734 mHandler.showServiceErrorAlert();
737 // the conversation is already closed, clear data in database
738 ContentResolver cr = mContext.getContentResolver();
739 cr.delete(ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId),
745 public void closeChatSessionIfInactive() {
746 if (mChatSession != null) {
748 mChatSession.leaveIfInactive();
749 } catch (RemoteException e) {
750 mHandler.showServiceErrorAlert();
755 public void viewProfile() {
756 Uri data = ContentUris.withAppendedId(Im.Contacts.CONTENT_URI, mChatId);
757 Intent intent = new Intent(Intent.ACTION_VIEW, data);
758 mScreen.startActivity(intent);
761 public void blockContact() {
762 // TODO: unify with codes in ContactListView
763 DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){
764 public void onClick(DialogInterface dialog, int whichButton) {
766 IImConnection conn = mApp.getConnection(mProviderId);
767 IContactListManager manager = conn.getContactListManager();
768 manager.blockContact(mUserName);
770 } catch (RemoteException e) {
771 mHandler.showServiceErrorAlert();
776 Resources r = getResources();
778 // The positive button is deliberately set as no so that
779 // the no is the default value
780 new AlertDialog.Builder(mContext)
781 .setTitle(R.string.confirm)
782 .setMessage(r.getString(R.string.confirm_block_contact, mNickName))
783 .setPositiveButton(R.string.no, null) // default button
784 .setNegativeButton(R.string.yes, confirmListener)
785 .setCancelable(false)
789 public long getProviderId() {
793 public long getAccountId() {
797 public String getUserName() {
801 public long getChatId () {
803 return mChatSession == null ? -1 : mChatSession.getId();
804 } catch (RemoteException e) {
805 mHandler.showServiceErrorAlert();
810 public IChatSession getCurrentChatSession() {
814 private IChatSessionManager getChatSessionManager(long providerId) {
815 if (mChatSessionMgr == null) {
816 IImConnection conn = mApp.getConnection(providerId);
819 mChatSessionMgr = conn.getChatSessionManager();
820 } catch (RemoteException e) {
821 mHandler.showServiceErrorAlert();
825 return mChatSessionMgr;
828 private IChatSession getChatSession(Cursor cursor) {
829 long providerId = cursor.getLong(PROVIDER_COLUMN);
830 String username = cursor.getString(USERNAME_COLUMN);
832 IChatSessionManager sessionMgr = getChatSessionManager(providerId);
833 if (sessionMgr != null) {
835 return sessionMgr.getChatSession(username);
836 } catch (RemoteException e) {
837 mHandler.showServiceErrorAlert();
843 boolean isGroupChat() {
844 return Im.Contacts.TYPE_GROUP == mType;
848 String msg = mEdtInput.getText().toString();
850 if (TextUtils.isEmpty(msg.trim())) {
854 if (mChatSession != null) {
856 mChatSession.sendMessage(msg);
857 mEdtInput.setText("");
858 mEdtInput.requestFocus();
860 } catch (RemoteException e) {
861 mHandler.showServiceErrorAlert();
865 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
867 Configuration config = getResources().getConfiguration();
868 if (config.orientation == config.ORIENTATION_LANDSCAPE) {
873 void registerChatListener() {
874 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
875 log("registerChatListener");
878 if (mChatSession != null) {
879 mChatSession.registerChatListener(mChatListener);
881 IImConnection conn = mApp.getConnection(mProviderId);
883 IContactListManager listMgr = conn.getContactListManager();
884 listMgr.registerContactListListener(mContactListListener);
886 mApp.dismissChatNotification(mProviderId, mUserName);
887 } catch (RemoteException e) {
888 Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
892 void unregisterChatListener() {
893 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
894 log("unregisterChatListener");
897 if (mChatSession != null) {
898 mChatSession.unregisterChatListener(mChatListener);
900 IImConnection conn = mApp.getConnection(mProviderId);
902 IContactListManager listMgr = conn.getContactListManager();
903 listMgr.unregisterContactListListener(mContactListListener);
905 } catch (RemoteException e) {
906 Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
910 void registerChatSessionListener() {
911 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
912 if (sessionMgr != null) {
913 mChatSessionListener = new ChatSessionListener();
915 sessionMgr.registerChatSessionListener(mChatSessionListener);
916 } catch (RemoteException e) {
917 mHandler.showServiceErrorAlert();
922 void unregisterChatSessionListener() {
923 if (mChatSessionListener != null) {
925 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
926 sessionMgr.unregisterChatSessionListener(mChatSessionListener);
927 // We unregister the listener when the chat session we are
928 // waiting for has been created or the activity is stopped.
929 // Clear the listener so that we won't unregister the listener
931 mChatSessionListener = null;
932 } catch (RemoteException e) {
933 mHandler.showServiceErrorAlert();
938 void updateWarningView() {
939 int visibility = View.GONE;
940 int iconVisibility = View.GONE;
941 String message = null;
945 IImConnection conn = mApp.getConnection(mProviderId);
946 isConnected = (conn == null) ? false
947 : conn.getState() != ImConnection.SUSPENDED;
948 } catch (RemoteException e) {
954 if (mType == Im.Contacts.TYPE_TEMPORARY) {
955 visibility = View.VISIBLE;
956 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName);
957 } else if (mPresenceStatus == Im.Presence.OFFLINE) {
958 visibility = View.VISIBLE;
959 message = mContext.getString(R.string.contact_offline_warning, mNickName);
962 visibility = View.VISIBLE;
963 iconVisibility = View.VISIBLE;
964 message = mContext.getString(R.string.disconnected_warning);
967 mStatusWarningView.setVisibility(visibility);
968 if (visibility == View.VISIBLE) {
969 mWarningIcon.setVisibility(iconVisibility);
970 mWarningText.setText(message);
975 public boolean dispatchKeyEvent(KeyEvent event) {
976 userActionDetected();
977 return super.dispatchKeyEvent(event);
981 public boolean dispatchTouchEvent(MotionEvent ev) {
982 userActionDetected();
983 return super.dispatchTouchEvent(ev);
987 public boolean dispatchTrackballEvent(MotionEvent ev) {
988 userActionDetected();
989 return super.dispatchTrackballEvent(ev);
992 private void userActionDetected() {
993 if (mChatSession != null) {
995 mChatSession.markAsRead();
996 } catch (RemoteException e) {
997 mHandler.showServiceErrorAlert();
1002 private final class ChatViewHandler extends SimpleAlertHandler {
1003 public ChatViewHandler() {
1008 public void handleMessage(Message msg) {
1009 long providerId = ((long)msg.arg1 << 32) | msg.arg2;
1010 if (providerId != mProviderId) {
1015 case ImApp.EVENT_CONNECTION_LOGGED_IN:
1016 log("Connection resumed");
1017 updateWarningView();
1019 case ImApp.EVENT_CONNECTION_SUSPENDED:
1020 log("Connection suspended");
1021 updateWarningView();
1025 super.handleMessage(msg);
1029 class ChatSessionListener extends ChatSessionListenerAdapter {
1031 public void onChatSessionCreated(IChatSession session) {
1033 if (session.isGroupChatSession()) {
1034 final long id = session.getId();
1035 unregisterChatSessionListener();
1036 mHandler.post(new Runnable() {
1041 } catch (RemoteException e) {
1042 mHandler.showServiceErrorAlert();
1047 public static class DeltaCursor implements Cursor {
1048 static final String DELTA_COLUMN_NAME = "delta";
1050 private Cursor mInnerCursor;
1051 private String[] mColumnNames;
1052 private int mDateColumn = -1;
1053 private int mDeltaColumn = -1;
1055 DeltaCursor(Cursor cursor) {
1056 mInnerCursor = cursor;
1058 String[] columnNames = cursor.getColumnNames();
1059 int len = columnNames.length;
1061 mColumnNames = new String[len + 1];
1063 for (int i = 0 ; i < len ; i++) {
1064 mColumnNames[i] = columnNames[i];
1065 if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
1071 mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
1073 //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
1074 // mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
1077 public int getCount() {
1078 return mInnerCursor.getCount();
1081 public int getPosition() {
1082 return mInnerCursor.getPosition();
1085 public boolean move(int offset) {
1086 return mInnerCursor.move(offset);
1089 public boolean moveToPosition(int position) {
1090 return mInnerCursor.moveToPosition(position);
1093 public boolean moveToFirst() {
1094 return mInnerCursor.moveToFirst();
1097 public boolean moveToLast() {
1098 return mInnerCursor.moveToLast();
1101 public boolean moveToNext() {
1102 return mInnerCursor.moveToNext();
1105 public boolean moveToPrevious() {
1106 return mInnerCursor.moveToPrevious();
1109 public boolean isFirst() {
1110 return mInnerCursor.isFirst();
1113 public boolean isLast() {
1114 return mInnerCursor.isLast();
1117 public boolean isBeforeFirst() {
1118 return mInnerCursor.isBeforeFirst();
1121 public boolean isAfterLast() {
1122 return mInnerCursor.isAfterLast();
1125 public boolean deleteRow() {
1126 return mInnerCursor.deleteRow();
1129 public int getColumnIndex(String columnName) {
1130 if (DELTA_COLUMN_NAME.equals(columnName)) {
1131 return mDeltaColumn;
1134 int columnIndex = mInnerCursor.getColumnIndex(columnName);
1138 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1139 if (DELTA_COLUMN_NAME.equals(columnName)) {
1140 return mDeltaColumn;
1143 return mInnerCursor.getColumnIndexOrThrow(columnName);
1146 public String getColumnName(int columnIndex) {
1147 if (columnIndex == mDeltaColumn) {
1148 return DELTA_COLUMN_NAME;
1151 return mInnerCursor.getColumnName(columnIndex);
1154 public int getColumnCount() {
1155 return mInnerCursor.getColumnCount() + 1;
1158 public boolean supportsUpdates() {
1159 return mInnerCursor.supportsUpdates();
1162 public boolean hasUpdates() {
1163 return mInnerCursor.hasUpdates();
1166 public boolean updateBlob(int columnIndex, byte[] value) {
1167 if (columnIndex == mDeltaColumn) {
1171 return mInnerCursor.updateBlob(columnIndex, value);
1174 public boolean updateString(int columnIndex, String value) {
1175 if (columnIndex == mDeltaColumn) {
1179 return mInnerCursor.updateString(columnIndex, value);
1182 public boolean updateShort(int columnIndex, short value) {
1183 if (columnIndex == mDeltaColumn) {
1187 return mInnerCursor.updateShort(columnIndex, value);
1190 public boolean updateInt(int columnIndex, int value) {
1191 if (columnIndex == mDeltaColumn) {
1195 return mInnerCursor.updateInt(columnIndex, value);
1198 public boolean updateLong(int columnIndex, long value) {
1199 if (columnIndex == mDeltaColumn) {
1203 return mInnerCursor.updateLong(columnIndex, value);
1206 public boolean updateFloat(int columnIndex, float value) {
1207 if (columnIndex == mDeltaColumn) {
1211 return mInnerCursor.updateFloat(columnIndex, value);
1214 public boolean updateDouble(int columnIndex, double value) {
1215 if (columnIndex == mDeltaColumn) {
1219 return mInnerCursor.updateDouble(columnIndex, value);
1222 public boolean updateToNull(int columnIndex) {
1223 if (columnIndex == mDeltaColumn) {
1227 return mInnerCursor.updateToNull(columnIndex);
1230 public boolean commitUpdates() {
1231 return mInnerCursor.commitUpdates();
1234 public boolean commitUpdates(Map<? extends Long,
1235 ? extends Map<String,Object>> values) {
1236 return mInnerCursor.commitUpdates(values);
1239 public void abortUpdates() {
1240 mInnerCursor.abortUpdates();
1243 public void deactivate() {
1244 mInnerCursor.deactivate();
1247 public boolean requery() {
1248 return mInnerCursor.requery();
1251 public void close() {
1252 mInnerCursor.close();
1255 public boolean isClosed() {
1256 return mInnerCursor.isClosed();
1259 public void registerContentObserver(ContentObserver observer) {
1260 mInnerCursor.registerContentObserver(observer);
1263 public void unregisterContentObserver(ContentObserver observer) {
1264 mInnerCursor.unregisterContentObserver(observer);
1267 public void registerDataSetObserver(DataSetObserver observer) {
1268 mInnerCursor.registerDataSetObserver(observer);
1271 public void unregisterDataSetObserver(DataSetObserver observer) {
1272 mInnerCursor.unregisterDataSetObserver(observer);
1275 public void setNotificationUri(ContentResolver cr, Uri uri) {
1276 mInnerCursor.setNotificationUri(cr, uri);
1279 public boolean getWantsAllOnMoveCalls() {
1280 return mInnerCursor.getWantsAllOnMoveCalls();
1283 public Bundle getExtras() {
1284 return mInnerCursor.getExtras();
1287 public Bundle respond(Bundle extras) {
1288 return mInnerCursor.respond(extras);
1291 public String[] getColumnNames() {
1292 return mColumnNames;
1295 private void checkPosition() {
1296 int pos = mInnerCursor.getPosition();
1297 int count = mInnerCursor.getCount();
1299 if (-1 == pos || count == pos) {
1300 throw new CursorIndexOutOfBoundsException(pos, count);
1304 public byte[] getBlob(int column) {
1307 if (column == mDeltaColumn) {
1311 return mInnerCursor.getBlob(column);
1314 public String getString(int column) {
1317 if (column == mDeltaColumn) {
1318 long value = getDeltaValue();
1319 return Long.toString(value);
1322 return mInnerCursor.getString(column);
1325 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1328 if (columnIndex == mDeltaColumn) {
1329 long value = getDeltaValue();
1330 String strValue = Long.toString(value);
1331 int len = strValue.length();
1332 char[] data = buffer.data;
1333 if (data == null || data.length < len) {
1334 buffer.data = strValue.toCharArray();
1336 strValue.getChars(0, len, data, 0);
1338 buffer.sizeCopied = strValue.length();
1340 mInnerCursor.copyStringToBuffer(columnIndex, buffer);
1344 public short getShort(int column) {
1347 if (column == mDeltaColumn) {
1348 return (short)getDeltaValue();
1351 return mInnerCursor.getShort(column);
1354 public int getInt(int column) {
1357 if (column == mDeltaColumn) {
1358 return (int)getDeltaValue();
1361 return mInnerCursor.getInt(column);
1364 public long getLong(int column) {
1365 //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1368 if (column == mDeltaColumn) {
1369 return getDeltaValue();
1372 return mInnerCursor.getLong(column);
1375 public float getFloat(int column) {
1378 if (column == mDeltaColumn) {
1379 return getDeltaValue();
1382 return mInnerCursor.getFloat(column);
1385 public double getDouble(int column) {
1388 if (column == mDeltaColumn) {
1389 return getDeltaValue();
1392 return mInnerCursor.getDouble(column);
1395 public boolean isNull(int column) {
1398 if (column == mDeltaColumn) {
1402 return mInnerCursor.isNull(column);
1405 private long getDeltaValue() {
1406 int pos = mInnerCursor.getPosition();
1407 //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1411 if (pos == getCount()-1) {
1412 t1 = mInnerCursor.getLong(mDateColumn);
1413 t2 = System.currentTimeMillis();
1415 mInnerCursor.moveToPosition(pos + 1);
1416 t2 = mInnerCursor.getLong(mDateColumn);
1417 mInnerCursor.moveToPosition(pos);
1418 t1 = mInnerCursor.getLong(mDateColumn);
1425 private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1426 private int mScrollState;
1427 private boolean mNeedRequeryCursor;
1429 private int mContactColumn;
1430 private int mBodyColumn;
1431 private int mDateColumn;
1432 private int mTypeColumn;
1433 private int mErrCodeColumn;
1434 private int mDeltaColumn;
1435 private ChatBackgroundMaker mBgMaker;
1437 private LayoutInflater mInflater;
1439 public MessageAdapter(Activity context, Cursor c) {
1440 super(context, c, false);
1441 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1442 mBgMaker = new ChatBackgroundMaker(context);
1444 resolveColumnIndex(c);
1448 private void resolveColumnIndex(Cursor c) {
1449 mContactColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.CONTACT);
1450 mBodyColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.BODY);
1451 mDateColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.DATE);
1452 mTypeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.TYPE);
1453 mErrCodeColumn = c.getColumnIndexOrThrow(Im.BaseMessageColumns.ERROR_CODE);
1454 mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME);
1458 public void changeCursor(Cursor cursor) {
1459 super.changeCursor(cursor);
1460 if (cursor != null) {
1461 resolveColumnIndex(cursor);
1466 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1467 return mInflater.inflate(R.layout.new_message_item, parent, false);
1471 public void bindView(View view, Context context, Cursor cursor) {
1472 MessageView chatMsgView = (MessageView) view;
1474 int type = cursor.getInt(mTypeColumn);
1475 String contact = isGroupChat() ? cursor.getString(mContactColumn) : mNickName;
1476 String body = cursor.getString(mBodyColumn);
1477 long delta = cursor.getLong(mDeltaColumn);
1478 boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL);
1479 Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null;
1482 case Im.MessageType.INCOMING:
1483 chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1486 case Im.MessageType.OUTGOING:
1487 case Im.MessageType.POSTPONED:
1488 int errCode = cursor.getInt(mErrCodeColumn);
1490 chatMsgView.bindErrorMessage(errCode);
1492 chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1497 chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1499 if (!isScrolling()) {
1500 mBgMaker.setBackground(chatMsgView, contact, type);
1503 // if showTimeStamp is false for the latest message, then set a timer to query the
1504 // cursor again in a minute, so we can update the last message timestamp if no new
1505 // message is received
1506 if (cursor.getPosition() == cursor.getCount()-1) {
1507 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
1508 log("delta = " + delta + ", showTs=" + showTimeStamp);
1510 if (!showTimeStamp) {
1511 scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1518 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1519 int totalItemCount) {
1523 public void onScrollStateChanged(AbsListView view, int scrollState) {
1524 int oldState = mScrollState;
1525 mScrollState = scrollState;
1527 if (mChatSession != null) {
1529 mChatSession.markAsRead();
1530 } catch (RemoteException e) {
1531 mHandler.showServiceErrorAlert();
1535 if (oldState == OnScrollListener.SCROLL_STATE_FLING) {
1536 if (mNeedRequeryCursor) {
1539 notifyDataSetChanged();
1544 boolean isScrolling() {
1545 return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1548 void setNeedRequeryCursor(boolean requeryCursor) {
1549 mNeedRequeryCursor = requeryCursor;