OSDN Git Service

Code drop from //branches/cupcake/...@124589
[android-x86/packages-apps-IM.git] / src / com / android / im / app / ChatView.java
1 /*
2  * Copyright (C) 2008 Esmertec AG.
3  * Copyright (C) 2008 The Android Open Source Project
4  *
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
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  */
17
18 package com.android.im.app;
19
20 import java.util.ArrayList;
21 import java.util.Date;
22 import java.util.Map;
23
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;
66
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;
82
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 = {
86         Im.Contacts._ID,
87         Im.Contacts.ACCOUNT,
88         Im.Contacts.PROVIDER,
89         Im.Contacts.USERNAME,
90         Im.Contacts.NICKNAME,
91         Im.Contacts.TYPE,
92         Im.Presence.PRESENCE_STATUS,
93         Im.Chats.LAST_UNREAD_MESSAGE,
94     };
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;
103
104     static final String[] INVITATION_PROJECT = {
105         Im.Invitation._ID,
106         Im.Invitation.PROVIDER,
107         Im.Invitation.SENDER,
108     };
109     static final int INVITATION_ID_COLUMN = 0;
110     static final int INVITATION_PROVIDER_COLUMN = 1;
111     static final int INVITATION_SENDER_COLUMN = 2;
112
113     static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
114
115     Markup mMarkup;
116
117     Activity mScreen;
118     ImApp mApp;
119     SimpleAlertHandler mHandler;
120     Cursor mCursor;
121
122     private ImageView   mStatusIcon;
123     private TextView    mTitle;
124     /*package*/ListView    mHistory;
125     EditText    mEdtInput;
126     private Button      mSendButton;
127     private View mStatusWarningView;
128     private ImageView mWarningIcon;
129     private TextView mWarningText;
130
131     private MessageAdapter mMessageAdapter;
132     private IChatSessionManager mChatSessionMgr;
133     private IChatSessionListener mChatSessionListener;
134
135     private IChatSession mChatSession;
136     private long mChatId;
137     int mType;
138     String mNickName;
139     String mUserName;
140     long mProviderId;
141     long mAccountId;
142     long mInvitationId;
143     private int mPresenceStatus;
144
145     private int mViewType;
146
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;
150
151     private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000;     // 1 minute
152     private static final int QUERY_TOKEN = 10;
153
154     // Async QueryHandler
155     private final class QueryHandler extends AsyncQueryHandler {
156         public QueryHandler(Context context) {
157             super(context.getContentResolver());
158         }
159
160         @Override
161         protected void onQueryComplete(int token, Object cookie, Cursor c) {
162             Cursor cursor = new DeltaCursor(c);
163
164             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
165                 log("onQueryComplete: cursor.count=" + cursor.getCount());
166             }
167
168             mMessageAdapter.changeCursor(cursor);
169         }
170     }
171     private QueryHandler mQueryHandler;
172
173     private class RequeryCallback implements Runnable {
174         public void run() {
175             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
176                 log("RequeryCallback");
177             }
178             requeryCursor();
179         }
180     }
181     private RequeryCallback mRequeryCallback = null;
182
183     private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
184         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
185             if (!(view instanceof MessageView)) {
186                 return;
187             }
188             URLSpan[] links = ((MessageView)view).getMessageLinks();
189             if (links.length == 0){
190                 return;
191             }
192
193             final ArrayList<String> linkUrls = new ArrayList<String>(links.length);
194             for (URLSpan u : links) {
195                 linkUrls.add(u.getURL());
196             }
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);
208                 }
209             });
210             b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
211                 public void onClick(DialogInterface dialog, int which) {
212                     dialog.dismiss();
213                 }
214             });
215             b.show();
216         }
217     };
218
219     private IChatListener mChatListener = new ChatListenerAdapter() {
220         @Override
221         public void onIncomingMessage(IChatSession ses,
222                 com.android.im.engine.Message msg) {
223             scheduleRequery(0);
224         }
225
226         @Override
227         public void onContactJoined(IChatSession ses, Contact contact) {
228             scheduleRequery(0);
229         }
230
231         @Override
232         public void onContactLeft(IChatSession ses, Contact contact) {
233             scheduleRequery(0);
234         }
235
236         @Override
237         public void onSendMessageError(IChatSession ses,
238                 com.android.im.engine.Message msg, ImErrorInfo error) {
239             scheduleRequery(0);
240         }
241     };
242
243     private Runnable mUpdateChatCallback = new Runnable() {
244         public void run() {
245             if (mCursor.requery() && mCursor.moveToFirst()) {
246                 updateChat();
247             }
248         }
249     };
250     private IContactListListener mContactListListener = new IContactListListener.Stub () {
251         public void onAllContactListsLoaded() {
252         }
253
254         public void onContactChange(int type, IContactList list, Contact contact){
255         }
256
257         public void onContactError(int errorType, ImErrorInfo error,
258                 String listName, Contact contact) {
259         }
260
261         public void onContactsPresenceUpdate(Contact[] contacts) {
262             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) {
263                 log("onContactsPresenceUpdate()");
264             }
265             for (Contact c : contacts) {
266                 if (c.getAddress().getFullName().equals(mUserName)) {
267                     mHandler.post(mUpdateChatCallback);
268                     scheduleRequery(0);
269                     break;
270                 }
271             }
272         }
273     };
274
275     static final void log(String msg) {
276         Log.d(ImApp.LOG_TAG, "<ChatView> " +msg);
277     }
278
279     public ChatView(Context context, AttributeSet attrs) {
280         super(context, attrs);
281         mScreen = (Activity) context;
282         mApp = ImApp.getApplication(mScreen);
283         mHandler = new ChatViewHandler();
284     }
285
286     void registerForConnEvents() {
287         mApp.registerForConnEvents(mHandler);
288     }
289
290     void unregisterForConnEvents() {
291         mApp.unregisterForConnEvents(mHandler);
292     }
293
294     @Override
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);
302
303         mStatusWarningView = findViewById(R.id.warning);
304         mWarningIcon = (ImageView)findViewById(R.id.warningIcon);
305         mWarningText = (TextView)findViewById(R.id.warningText);
306
307         Button acceptInvitation = (Button)findViewById(R.id.btnAccept);
308         Button declineInvitation= (Button)findViewById(R.id.btnDecline);
309
310         Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription);
311         Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription);
312
313         acceptInvitation.setOnClickListener(new OnClickListener() {
314             public void onClick(View v) {
315                 acceptInvitation();
316             }
317         });
318         declineInvitation.setOnClickListener(new OnClickListener() {
319             public void onClick(View v) {
320                 declineInvitation();
321             }
322         });
323
324         approveSubscription.setOnClickListener(new OnClickListener(){
325             public void onClick(View v) {
326                 approveSubscription();
327             }
328         });
329         declineSubscription.setOnClickListener(new OnClickListener(){
330             public void onClick(View v) {
331                 declineSubscription();
332             }
333         });
334
335         mEdtInput.setOnKeyListener(new OnKeyListener(){
336             public boolean onKey(View v, int keyCode, KeyEvent event) {
337                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
338                     switch (keyCode) {
339                         case KeyEvent.KEYCODE_DPAD_CENTER:
340                             sendMessage();
341                             return true;
342
343                         case KeyEvent.KEYCODE_ENTER:
344                             if (event.isAltPressed()) {
345                                 mEdtInput.append("\n");
346                             } else {
347                                 sendMessage();
348                             }
349                             return true;
350                     }
351                 }
352                 return false;
353             }
354         });
355         mSendButton.setOnClickListener(new OnClickListener() {
356             public void onClick(View v) {
357                 sendMessage();
358             }
359         });
360     }
361
362     public void onResume(){
363         if (mViewType == VIEW_TYPE_CHAT) {
364             Cursor cursor = getMessageCursor();
365             if (cursor == null) {
366                 startQuery();
367             } else {
368                 requeryCursor();
369             }
370             updateWarningView();
371         }
372         registerChatListener();
373         registerForConnEvents();
374     }
375
376     public void onPause(){
377         Cursor cursor = getMessageCursor();
378         if (cursor != null) {
379             cursor.deactivate();
380         }
381         cancelRequery();
382         if (mViewType == VIEW_TYPE_CHAT) {
383             markAsRead();
384         }
385         unregisterChatListener();
386         unregisterForConnEvents();
387         unregisterChatSessionListener();
388     }
389
390     void updateChat() {
391         setViewType(VIEW_TYPE_CHAT);
392
393         long oldChatId = mChatId;
394
395         updateContactInfo();
396
397         setStatusIcon();
398         setTitle();
399
400         IImConnection conn = mApp.getConnection(mProviderId);
401         if (conn == null) {
402             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out");
403             mScreen.finish();
404             return;
405         }
406
407         BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
408         mHistory.setBackgroundDrawable(
409                 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK));
410
411         if (mMarkup == null) {
412             mMarkup = new Markup(brandingRes);
413         }
414
415         if (mMessageAdapter == null) {
416             mMessageAdapter = new MessageAdapter(mScreen, null);
417             mHistory.setAdapter(mMessageAdapter);
418         }
419
420         // only change the message adapter when we switch to another chat
421         if (mChatId != oldChatId) {
422             startQuery();
423             mEdtInput.setText("");
424         }
425
426         updateWarningView();
427     }
428
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);
437     }
438
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();
446             if(c != null) {
447                 while(c.moveToNext()) {
448                     buf.append(c.getString(0));
449                     if(!c.isLast()) {
450                         buf.append(',');
451                     }
452                 }
453                 c.close();
454             }
455             mTitle.setText(mContext.getString(R.string.chat_with, buf.toString()));
456         } else {
457             mTitle.setText(mContext.getString(R.string.chat_with, mNickName));
458         }
459     }
460
461     private void setStatusIcon() {
462         if (mType == Im.Contacts.TYPE_GROUP) {
463             // hide the status icon for group chat.
464             mStatusIcon.setVisibility(GONE);
465         } else {
466             mStatusIcon.setVisibility(VISIBLE);
467             BrandingResources brandingRes = mApp.getBrandingResource(mProviderId);
468             int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus);
469             mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId));
470         }
471     }
472
473     public void bindChat(long chatId) {
474         if (mCursor != null) {
475             mCursor.deactivate();
476         }
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);
482             }
483             mScreen.finish();
484             return;
485         } else {
486             mChatSession = getChatSession(mCursor);
487             updateChat();
488             registerChatListener();
489         }
490     }
491
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);
499             }
500             mScreen.finish();
501         } else {
502             setViewType(VIEW_TYPE_INVITATION);
503
504             mInvitationId = cursor.getLong(INVITATION_ID_COLUMN);
505             mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN);
506             String sender = cursor.getString(INVITATION_SENDER_COLUMN);
507
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));
511         }
512
513         if (cursor != null) {
514             cursor.close();
515         }
516     }
517
518     public void bindSubscription(long providerId, String from) {
519         mProviderId = providerId;
520         mUserName = from;
521
522         setViewType(VIEW_TYPE_SUBSCRIPTION);
523
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));
528     }
529
530     void acceptInvitation() {
531         try {
532
533             IImConnection conn = mApp.getConnection(mProviderId);
534             if (conn != null) {
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);
539             }
540         } catch (RemoteException e) {
541             mHandler.showServiceErrorAlert();
542         }
543     }
544
545     void declineInvitation() {
546         try {
547             IImConnection conn = mApp.getConnection(mProviderId);
548             if (conn != null) {
549                 conn.rejectInvitation(mInvitationId);
550             }
551             mScreen.finish();
552         } catch (RemoteException e) {
553             mHandler.showServiceErrorAlert();
554         }
555     }
556
557     void approveSubscription() {
558         IImConnection conn = mApp.getConnection(mProviderId);
559         try {
560             IContactListManager manager = conn.getContactListManager();
561             manager.approveSubscription(mUserName);
562         } catch (RemoteException ex) {
563             mHandler.showServiceErrorAlert();
564         }
565         mScreen.finish();
566     }
567
568     void declineSubscription() {
569         IImConnection conn = mApp.getConnection(mProviderId);
570         try {
571             IContactListManager manager = conn.getContactListManager();
572             manager.declineSubscription(mUserName);
573         } catch (RemoteException ex) {
574             mHandler.showServiceErrorAlert();
575         }
576         mScreen.finish();
577     }
578
579     private void setViewType(int type) {
580         mViewType = 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();
593         }
594     }
595
596     private void setChatViewEnabled(boolean enabled) {
597         mEdtInput.setEnabled(enabled);
598         mSendButton.setEnabled(enabled);
599         if (enabled) {
600             mEdtInput.requestFocus();
601         } else {
602             mHistory.setAdapter(null);
603         }
604     }
605
606     private void markAsRead() {
607         ContentValues values = new ContentValues(1);
608         values.put(Im.Chats.LAST_UNREAD_MESSAGE, (String)null);
609
610         ContentResolver cr = mContext.getContentResolver();
611         Uri uri = ContentUris.withAppendedId(Im.Chats.CONTENT_URI, mChatId);
612         cr.update(uri, values, null, null);
613     }
614
615     private void startQuery() {
616         if (mQueryHandler == null) {
617             mQueryHandler = new QueryHandler(mContext);
618         } else {
619             // Cancel any pending queries
620             mQueryHandler.cancelOperation(QUERY_TOKEN);
621         }
622
623         Uri uri;
624         if (Im.Contacts.TYPE_GROUP == mType) {
625             uri = ContentUris.withAppendedId(Im.GroupMessages.CONTENT_URI_GROUP_MESSAGES_BY, mChatId);
626         } else {
627             uri = Im.Messages.getContentUriByContact(mProviderId, mAccountId, mUserName);
628         }
629
630         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
631             log("queryCursor: uri=" + uri);
632         }
633
634         mQueryHandler.startQuery(QUERY_TOKEN, null,
635                 uri,
636                 null,
637                 null /* selection */,
638                 null /* selection args */,
639                 null);
640     }
641
642     void scheduleRequery(long interval) {
643         if (mRequeryCallback == null) {
644             mRequeryCallback = new RequeryCallback();
645         } else {
646             mHandler.removeCallbacks(mRequeryCallback);
647         }
648
649         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
650             log("scheduleRequery");
651         }
652         mHandler.postDelayed(mRequeryCallback, interval);
653     }
654
655     void cancelRequery() {
656         if (mRequeryCallback != null) {
657             if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
658                 log("cancelRequery");
659             }
660             mHandler.removeCallbacks(mRequeryCallback);
661             mRequeryCallback = null;
662         }
663     }
664
665     void requeryCursor() {
666         if (mMessageAdapter.isScrolling()) {
667             mMessageAdapter.setNeedRequeryCursor(true);
668             return;
669         }
670         // TODO: async query?
671         Cursor cursor = getMessageCursor();
672         if (cursor != null) {
673             cursor.requery();
674         }
675     }
676
677     private Cursor getMessageCursor() {
678         return mMessageAdapter == null ? null : mMessageAdapter.getCursor();
679     }
680
681     public void insertSmiley(String smiley) {
682         mEdtInput.append(mMarkup.applyEmoticons(smiley));
683     }
684
685     public void closeChatSession() {
686         if (mChatSession != null) {
687             try {
688                 mChatSession.leave();
689             } catch (RemoteException e) {
690                 mHandler.showServiceErrorAlert();
691             }
692         } else {
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),
696                     null, null);
697         }
698         mScreen.finish();
699     }
700
701     public void closeChatSessionIfInactive() {
702         if (mChatSession != null) {
703             try {
704                 mChatSession.leaveIfInactive();
705             } catch (RemoteException e) {
706                 mHandler.showServiceErrorAlert();
707             }
708         }
709     }
710
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);
715     }
716
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) {
721                 try {
722                     IImConnection conn = mApp.getConnection(mProviderId);
723                     IContactListManager manager = conn.getContactListManager();
724                     manager.blockContact(mUserName);
725                     mScreen.finish();
726                 } catch (RemoteException e) {
727                     mHandler.showServiceErrorAlert();
728                 }
729             }
730         };
731
732         Resources r = getResources();
733
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)
742             .show();
743     }
744
745     public long getProviderId() {
746         return mProviderId;
747     }
748
749     public long getAccountId() {
750         return mAccountId;
751     }
752
753     public String getUserName() {
754         return mUserName;
755     }
756
757     public long getChatId () {
758         try {
759             return mChatSession == null ? -1 : mChatSession.getId();
760         } catch (RemoteException e) {
761             mHandler.showServiceErrorAlert();
762             return -1;
763         }
764     }
765
766     public IChatSession getCurrentChatSession() {
767         return mChatSession;
768     }
769
770     private IChatSessionManager getChatSessionManager(long providerId) {
771         if (mChatSessionMgr == null) {
772             IImConnection conn = mApp.getConnection(providerId);
773             if (conn != null) {
774                 try {
775                     mChatSessionMgr = conn.getChatSessionManager();
776                 } catch (RemoteException e) {
777                     mHandler.showServiceErrorAlert();
778                 }
779             }
780         }
781         return mChatSessionMgr;
782     }
783
784     private IChatSession getChatSession(Cursor cursor) {
785         long providerId = cursor.getLong(PROVIDER_COLUMN);
786         String username = cursor.getString(USERNAME_COLUMN);
787
788         IChatSessionManager sessionMgr = getChatSessionManager(providerId);
789         if (sessionMgr != null) {
790             try {
791                 return sessionMgr.getChatSession(username);
792             } catch (RemoteException e) {
793                 mHandler.showServiceErrorAlert();
794             }
795         }
796         return null;
797     }
798
799     boolean isGroupChat() {
800         return Im.Contacts.TYPE_GROUP == mType;
801     }
802
803     void sendMessage() {
804         String msg = mEdtInput.getText().toString();
805         if (mChatSession != null && !TextUtils.isEmpty(msg.trim())) {
806             try {
807                 mChatSession.sendMessage(msg);
808                 mEdtInput.setText("");
809                 mEdtInput.requestFocus();
810                 requeryCursor();
811             } catch (RemoteException e) {
812                 mHandler.showServiceErrorAlert();
813             }
814         }
815     }
816
817     void registerChatListener() {
818         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
819             log("registerChatListener");
820         }
821         try {
822             if (mChatSession != null) {
823                 mChatSession.registerChatListener(mChatListener);
824             }
825             IImConnection conn = mApp.getConnection(mProviderId);
826             if (conn != null) {
827                 IContactListManager listMgr = conn.getContactListManager();
828                 listMgr.registerContactListListener(mContactListListener);
829             }
830             mApp.dismissNotifications(mProviderId);
831         } catch (RemoteException e) {
832             Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage());
833         }
834     }
835
836     void unregisterChatListener() {
837         if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){
838             log("unregisterChatListener");
839         }
840         try {
841             if (mChatSession != null) {
842                 mChatSession.unregisterChatListener(mChatListener);
843             }
844             IImConnection conn = mApp.getConnection(mProviderId);
845             if (conn != null) {
846                 IContactListManager listMgr = conn.getContactListManager();
847                 listMgr.unregisterContactListListener(mContactListListener);
848             }
849         } catch (RemoteException e) {
850             Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage());
851         }
852     }
853
854     void registerChatSessionListener() {
855         IChatSessionManager sessionMgr = getChatSessionManager(mProviderId);
856         if (sessionMgr != null) {
857             mChatSessionListener = new ChatSessionListener();
858             try {
859                 sessionMgr.registerChatSessionListener(mChatSessionListener);
860             } catch (RemoteException e) {
861                 mHandler.showServiceErrorAlert();
862             }
863         }
864     }
865
866     void unregisterChatSessionListener() {
867         if (mChatSessionListener != null) {
868             try {
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
874                 // twice.
875                 mChatSessionListener = null;
876             } catch (RemoteException e) {
877                 mHandler.showServiceErrorAlert();
878             }
879         }
880     }
881
882     void updateWarningView() {
883         int visibility = View.GONE;
884         int iconVisibility = View.GONE;
885         String message = null;
886         boolean isConnected;
887
888         try {
889             IImConnection conn = mApp.getConnection(mProviderId);
890             isConnected = (conn == null) ? false
891                     : conn.getState() != ImConnection.SUSPENDED;
892         } catch (RemoteException e) {
893             // do nothing
894             return;
895         }
896
897         if (isConnected) {
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);
904             }
905         } else {
906             visibility = View.VISIBLE;
907             iconVisibility = View.VISIBLE;
908             message = mContext.getString(R.string.disconnected_warning);
909         }
910
911         mStatusWarningView.setVisibility(visibility);
912         if (visibility == View.VISIBLE) {
913             mWarningIcon.setVisibility(iconVisibility);
914             mWarningText.setText(message);
915         }
916     }
917
918     private final class ChatViewHandler extends SimpleAlertHandler {
919         public ChatViewHandler() {
920             super(mScreen);
921         }
922
923         @Override
924         public void handleMessage(Message msg) {
925             long providerId = ((long)msg.arg1 << 32) | msg.arg2;
926             if (providerId != mProviderId) {
927                 return;
928             }
929
930             switch(msg.what) {
931             case ImApp.EVENT_CONNECTION_LOGGED_IN:
932                 log("Connection resumed");
933                 updateWarningView();
934                 return;
935             case ImApp.EVENT_CONNECTION_SUSPENDED:
936                 log("Connection suspended");
937                 updateWarningView();
938                 return;
939             }
940
941             super.handleMessage(msg);
942         }
943     }
944
945     class ChatSessionListener extends ChatSessionListenerAdapter {
946         @Override
947         public void onChatSessionCreated(IChatSession session) {
948             try {
949                 if (session.isGroupChatSession()) {
950                     final long id = session.getId();
951                     unregisterChatSessionListener();
952                     mHandler.post(new Runnable() {
953                         public void run() {
954                             bindChat(id);
955                         }});
956                 }
957             } catch (RemoteException e) {
958                 mHandler.showServiceErrorAlert();
959             }
960         }
961     }
962
963     public static class DeltaCursor implements Cursor {
964         static final String DELTA_COLUMN_NAME = "delta";
965
966         private Cursor mCursor;
967         private String[] mColumnNames;
968         private int mDateColumn = -1;
969         private int mDeltaColumn = -1;
970
971         DeltaCursor(Cursor cursor) {
972             mCursor = cursor;
973
974             String[] columnNames = cursor.getColumnNames();
975             int len = columnNames.length;
976
977             mColumnNames = new String[len + 1];
978
979             for (int i = 0 ; i < len ; i++) {
980                 mColumnNames[i] = columnNames[i];
981                 if (mColumnNames[i].equals(Im.BaseMessageColumns.DATE)) {
982                     mDateColumn = i;
983                 }
984             }
985
986             mDeltaColumn = len;
987             mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME;
988
989             //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" +
990             //        mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]);
991         }
992
993         public int getCount() {
994             return mCursor.getCount();
995         }
996
997         public int getPosition() {
998             return mCursor.getPosition();
999         }
1000
1001         public boolean move(int offset) {
1002             return mCursor.move(offset);
1003         }
1004
1005         public boolean moveToPosition(int position) {
1006             return mCursor.moveToPosition(position);
1007         }
1008
1009         public boolean moveToFirst() {
1010             return mCursor.moveToFirst();
1011         }
1012
1013         public boolean moveToLast() {
1014             return mCursor.moveToLast();
1015         }
1016
1017         public boolean moveToNext() {
1018             return mCursor.moveToNext();
1019         }
1020
1021         public boolean moveToPrevious() {
1022             return mCursor.moveToPrevious();
1023         }
1024
1025         public boolean isFirst() {
1026             return mCursor.isFirst();
1027         }
1028
1029         public boolean isLast() {
1030             return mCursor.isLast();
1031         }
1032
1033         public boolean isBeforeFirst() {
1034             return mCursor.isBeforeFirst();
1035         }
1036
1037         public boolean isAfterLast() {
1038             return mCursor.isAfterLast();
1039         }
1040
1041         public boolean deleteRow() {
1042             return mCursor.deleteRow();
1043         }
1044
1045         public int getColumnIndex(String columnName) {
1046             if (DELTA_COLUMN_NAME.equals(columnName)) {
1047                 return mDeltaColumn;
1048             }
1049
1050             int columnIndex = mCursor.getColumnIndex(columnName);
1051             return columnIndex;
1052         }
1053
1054         public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1055             if (DELTA_COLUMN_NAME.equals(columnName)) {
1056                 return mDeltaColumn;
1057             }
1058
1059             return mCursor.getColumnIndexOrThrow(columnName);
1060         }
1061
1062         public String getColumnName(int columnIndex) {
1063             if (columnIndex == mDeltaColumn) {
1064                 return DELTA_COLUMN_NAME;
1065             }
1066
1067             return mCursor.getColumnName(columnIndex);
1068         }
1069
1070         public int getColumnCount() {
1071             return mCursor.getColumnCount() + 1;
1072         }
1073
1074         public boolean supportsUpdates() {
1075             return mCursor.supportsUpdates();
1076         }
1077
1078         public boolean hasUpdates() {
1079             return mCursor.hasUpdates();
1080         }
1081
1082         public boolean updateBlob(int columnIndex, byte[] value) {
1083             if (columnIndex == mDeltaColumn) {
1084                 return false;
1085             }
1086
1087             return mCursor.updateBlob(columnIndex, value);
1088         }
1089
1090         public boolean updateString(int columnIndex, String value) {
1091             if (columnIndex == mDeltaColumn) {
1092                 return false;
1093             }
1094
1095             return mCursor.updateString(columnIndex, value);
1096         }
1097
1098         public boolean updateShort(int columnIndex, short value) {
1099             if (columnIndex == mDeltaColumn) {
1100                 return false;
1101             }
1102
1103             return mCursor.updateShort(columnIndex, value);
1104         }
1105
1106         public boolean updateInt(int columnIndex, int value) {
1107             if (columnIndex == mDeltaColumn) {
1108                 return false;
1109             }
1110
1111             return mCursor.updateInt(columnIndex, value);
1112         }
1113
1114         public boolean updateLong(int columnIndex, long value) {
1115             if (columnIndex == mDeltaColumn) {
1116                 return false;
1117             }
1118
1119             return mCursor.updateLong(columnIndex, value);
1120         }
1121
1122         public boolean updateFloat(int columnIndex, float value) {
1123             if (columnIndex == mDeltaColumn) {
1124                 return false;
1125             }
1126
1127             return mCursor.updateFloat(columnIndex, value);
1128         }
1129
1130         public boolean updateDouble(int columnIndex, double value) {
1131             if (columnIndex == mDeltaColumn) {
1132                 return false;
1133             }
1134
1135             return mCursor.updateDouble(columnIndex, value);
1136         }
1137
1138         public boolean updateToNull(int columnIndex) {
1139             if (columnIndex == mDeltaColumn) {
1140                 return false;
1141             }
1142
1143             return mCursor.updateToNull(columnIndex);
1144         }
1145
1146         public boolean commitUpdates() {
1147             return mCursor.commitUpdates();
1148         }
1149
1150         public boolean commitUpdates(Map<? extends Long,
1151                 ? extends Map<String,Object>> values) {
1152             return mCursor.commitUpdates(values);
1153         }
1154
1155         public void abortUpdates() {
1156             mCursor.abortUpdates();
1157         }
1158
1159         public void deactivate() {
1160             mCursor.deactivate();
1161         }
1162
1163         public boolean requery() {
1164             return mCursor.requery();
1165         }
1166
1167         public void close() {
1168             mCursor.close();
1169         }
1170
1171         public boolean isClosed() {
1172             return mCursor.isClosed();
1173         }
1174
1175         public void registerContentObserver(ContentObserver observer) {
1176             mCursor.registerContentObserver(observer);
1177         }
1178
1179         public void unregisterContentObserver(ContentObserver observer) {
1180             mCursor.unregisterContentObserver(observer);
1181         }
1182
1183         public void registerDataSetObserver(DataSetObserver observer) {
1184             mCursor.registerDataSetObserver(observer);
1185         }
1186
1187         public void unregisterDataSetObserver(DataSetObserver observer) {
1188             mCursor.unregisterDataSetObserver(observer);
1189         }
1190
1191         public void setNotificationUri(ContentResolver cr, Uri uri) {
1192             mCursor.setNotificationUri(cr, uri);
1193         }
1194
1195         public boolean getWantsAllOnMoveCalls() {
1196             return mCursor.getWantsAllOnMoveCalls();
1197         }
1198
1199         public Bundle getExtras() {
1200             return mCursor.getExtras();
1201         }
1202
1203         public Bundle respond(Bundle extras) {
1204             return mCursor.respond(extras);
1205         }
1206
1207         public String[] getColumnNames() {
1208             return mColumnNames;
1209         }
1210
1211         private void checkPosition() {
1212             int pos = mCursor.getPosition();
1213             int count = mCursor.getCount();
1214
1215             if (-1 == pos || count == pos) {
1216                 throw new CursorIndexOutOfBoundsException(pos, count);
1217             }
1218         }
1219
1220         public byte[] getBlob(int column) {
1221             checkPosition();
1222
1223             if (column == mDeltaColumn) {
1224                 return null;
1225             }
1226
1227             return mCursor.getBlob(column);
1228         }
1229
1230         public String getString(int column) {
1231             checkPosition();
1232
1233             if (column == mDeltaColumn) {
1234                 long value = getDeltaValue();
1235                 return Long.toString(value);
1236             }
1237
1238             return mCursor.getString(column);
1239         }
1240
1241         public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1242             checkPosition();
1243
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();
1251                 } else {
1252                     strValue.getChars(0, len, data, 0);
1253                 }
1254                 buffer.sizeCopied = strValue.length();
1255             } else {
1256                 mCursor.copyStringToBuffer(columnIndex, buffer);
1257             }
1258         }
1259
1260         public short getShort(int column) {
1261             checkPosition();
1262
1263             if (column == mDeltaColumn) {
1264                 return (short)getDeltaValue();
1265             }
1266
1267             return mCursor.getShort(column);
1268         }
1269
1270         public int getInt(int column) {
1271             checkPosition();
1272
1273             if (column == mDeltaColumn) {
1274                 return (int)getDeltaValue();
1275             }
1276
1277             return mCursor.getInt(column);
1278         }
1279
1280         public long getLong(int column) {
1281         //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn);
1282             checkPosition();
1283
1284             if (column == mDeltaColumn) {
1285                 return getDeltaValue();
1286             }
1287
1288             return mCursor.getLong(column);
1289         }
1290
1291         public float getFloat(int column) {
1292             checkPosition();
1293
1294             if (column == mDeltaColumn) {
1295                 return getDeltaValue();
1296             }
1297
1298             return mCursor.getFloat(column);
1299         }
1300
1301         public double getDouble(int column) {
1302             checkPosition();
1303
1304             if (column == mDeltaColumn) {
1305                 return getDeltaValue();
1306             }
1307
1308             return mCursor.getDouble(column);
1309         }
1310
1311         public boolean isNull(int column) {
1312             checkPosition();
1313
1314             if (column == mDeltaColumn) {
1315                 return false;
1316             }
1317
1318             return mCursor.isNull(column);
1319         }
1320
1321         private long getDeltaValue() {
1322             int pos = mCursor.getPosition();
1323             //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos);
1324
1325             long t2, t1;
1326
1327             if (pos == getCount()-1) {
1328                 t1 = mCursor.getLong(mDateColumn);
1329                 t2 = System.currentTimeMillis();
1330             } else {
1331                 mCursor.moveToPosition(pos + 1);
1332                 t2 = mCursor.getLong(mDateColumn);
1333                 mCursor.moveToPosition(pos);
1334                 t1 = mCursor.getLong(mDateColumn);
1335             }
1336
1337             return t2 - t1;
1338         }
1339     }
1340
1341     private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener {
1342         private int mScrollState;
1343         private boolean mNeedRequeryCursor;
1344
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;
1352
1353         private LayoutInflater mInflater;
1354
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);
1359             if (c != null) {
1360                 resolveColumnIndex(c);
1361             }
1362         }
1363
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);
1371         }
1372
1373         @Override
1374         public void changeCursor(Cursor cursor) {
1375             super.changeCursor(cursor);
1376             if (cursor != null) {
1377                 resolveColumnIndex(cursor);
1378             }
1379         }
1380
1381         @Override
1382         public View newView(Context context, Cursor cursor, ViewGroup parent) {
1383             return mInflater.inflate(R.layout.new_message_item, parent, false);
1384         }
1385
1386         @Override
1387         public void bindView(View view, Context context, Cursor cursor) {
1388             MessageView chatMsgView = (MessageView) view;
1389
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;
1396
1397             switch (type) {
1398                 case Im.MessageType.INCOMING:
1399                     chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling());
1400                     break;
1401
1402                 case Im.MessageType.OUTGOING:
1403                 case Im.MessageType.POSTPONED:
1404                     int errCode = cursor.getInt(mErrCodeColumn);
1405                     if (errCode != 0) {
1406                         chatMsgView.bindErrorMessage(errCode);
1407                     } else {
1408                         chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling());
1409                     }
1410                     break;
1411
1412                 default:
1413                     chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling());
1414             }
1415             if (!isScrolling()) {
1416                 mBgMaker.setBackground(chatMsgView, contact, type);
1417             }
1418
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);
1425                 }
1426                 if (!showTimeStamp) {
1427                     scheduleRequery(SHOW_TIME_STAMP_INTERVAL);
1428                 } else {
1429                     cancelRequery();
1430                 }
1431             }
1432         }
1433
1434         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1435                 int totalItemCount) {
1436             // do nothing
1437         }
1438
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) {
1444                     requeryCursor();
1445                 } else {
1446                     notifyDataSetChanged();
1447                 }
1448             }
1449         }
1450
1451         boolean isScrolling() {
1452             return mScrollState == OnScrollListener.SCROLL_STATE_FLING;
1453         }
1454
1455         void setNeedRequeryCursor(boolean requeryCursor) {
1456             mNeedRequeryCursor = requeryCursor;
1457         }
1458     }
1459 }