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 18package com.android.im.app; 19 20import java.util.ArrayList; 21import java.util.Date; 22import java.util.Map; 23 24import android.app.Activity; 25import android.app.AlertDialog; 26import android.content.AsyncQueryHandler; 27import android.content.ContentResolver; 28import android.content.ContentUris; 29import android.content.Context; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.content.res.Resources; 33import android.content.res.Configuration; 34import android.database.ContentObserver; 35import android.database.Cursor; 36import android.database.CursorIndexOutOfBoundsException; 37import android.database.DataSetObserver; 38import android.database.CharArrayBuffer; 39import android.graphics.Typeface; 40import android.net.Uri; 41import android.os.Bundle; 42import android.os.Message; 43import android.os.RemoteException; 44import android.provider.Browser; 45import android.text.Editable; 46import android.text.TextUtils; 47import android.text.TextWatcher; 48import android.text.style.StyleSpan; 49import android.text.style.URLSpan; 50import android.util.AttributeSet; 51import android.util.Log; 52import android.view.KeyEvent; 53import android.view.LayoutInflater; 54import android.view.MotionEvent; 55import android.view.View; 56import android.view.ViewGroup; 57import android.view.inputmethod.InputMethodManager; 58import android.widget.AbsListView; 59import android.widget.AdapterView; 60import android.widget.ArrayAdapter; 61import android.widget.Button; 62import android.widget.CursorAdapter; 63import android.widget.EditText; 64import android.widget.ImageView; 65import android.widget.LinearLayout; 66import android.widget.ListView; 67import android.widget.TextView; 68import android.widget.AbsListView.OnScrollListener; 69import android.widget.AdapterView.OnItemClickListener; 70 71import com.android.im.IChatListener; 72import com.android.im.IChatSession; 73import com.android.im.IChatSessionListener; 74import com.android.im.IChatSessionManager; 75import com.android.im.IContactList; 76import com.android.im.IContactListListener; 77import com.android.im.IContactListManager; 78import com.android.im.IImConnection; 79import com.android.im.R; 80import com.android.im.app.adapter.ChatListenerAdapter; 81import com.android.im.app.adapter.ChatSessionListenerAdapter; 82import com.android.im.engine.Contact; 83import com.android.im.engine.ImConnection; 84import com.android.im.engine.ImErrorInfo; 85import com.android.im.plugin.BrandingResourceIDs; 86import com.android.im.provider.Imps; 87 88public class ChatView extends LinearLayout { 89 // This projection and index are set for the query of active chats 90 static final String[] CHAT_PROJECTION = { 91 Imps.Contacts._ID, 92 Imps.Contacts.ACCOUNT, 93 Imps.Contacts.PROVIDER, 94 Imps.Contacts.USERNAME, 95 Imps.Contacts.NICKNAME, 96 Imps.Contacts.TYPE, 97 Imps.Presence.PRESENCE_STATUS, 98 Imps.Chats.LAST_UNREAD_MESSAGE, 99 }; 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; 108 109 static final String[] INVITATION_PROJECT = { 110 Imps.Invitation._ID, 111 Imps.Invitation.PROVIDER, 112 Imps.Invitation.SENDER, 113 }; 114 static final int INVITATION_ID_COLUMN = 0; 115 static final int INVITATION_PROVIDER_COLUMN = 1; 116 static final int INVITATION_SENDER_COLUMN = 2; 117 118 static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD); 119 120 Markup mMarkup; 121 122 Activity mScreen; 123 ImApp mApp; 124 SimpleAlertHandler mHandler; 125 Cursor mCursor; 126 127 private ImageView mStatusIcon; 128 private TextView mTitle; 129 /*package*/ListView mHistory; 130 EditText mEdtInput; 131 private Button mSendButton; 132 private View mStatusWarningView; 133 private ImageView mWarningIcon; 134 private TextView mWarningText; 135 136 private MessageAdapter mMessageAdapter; 137 private IChatSessionManager mChatSessionMgr; 138 private IChatSessionListener mChatSessionListener; 139 140 private IChatSession mChatSession; 141 private long mChatId; 142 int mType; 143 String mNickName; 144 String mUserName; 145 long mProviderId; 146 long mAccountId; 147 long mInvitationId; 148 private int mPresenceStatus; 149 150 private int mViewType; 151 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; 155 156 private static final long SHOW_TIME_STAMP_INTERVAL = 60 * 1000; // 1 minute 157 private static final int QUERY_TOKEN = 10; 158 159 // Async QueryHandler 160 private final class QueryHandler extends AsyncQueryHandler { 161 public QueryHandler(Context context) { 162 super(context.getContentResolver()); 163 } 164 165 @Override 166 protected void onQueryComplete(int token, Object cookie, Cursor c) { 167 Cursor cursor = new DeltaCursor(c); 168 169 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 170 log("onQueryComplete: cursor.count=" + cursor.getCount()); 171 } 172 173 mMessageAdapter.changeCursor(cursor); 174 } 175 } 176 private QueryHandler mQueryHandler; 177 178 private class RequeryCallback implements Runnable { 179 public void run() { 180 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 181 log("RequeryCallback"); 182 } 183 requeryCursor(); 184 } 185 } 186 private RequeryCallback mRequeryCallback = null; 187 188 private OnItemClickListener mOnItemClickListener = new OnItemClickListener() { 189 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 190 if (!(view instanceof MessageView)) { 191 return; 192 } 193 URLSpan[] links = ((MessageView)view).getMessageLinks(); 194 if (links.length == 0){ 195 return; 196 } 197 198 final ArrayList<String> linkUrls = new ArrayList<String>(links.length); 199 for (URLSpan u : links) { 200 linkUrls.add(u.getURL()); 201 } 202 ArrayAdapter<String> a = new ArrayAdapter<String>(mScreen, 203 android.R.layout.select_dialog_item, linkUrls); 204 AlertDialog.Builder b = new AlertDialog.Builder(mScreen); 205 b.setTitle(R.string.select_link_title); 206 b.setCancelable(true); 207 b.setAdapter(a, new DialogInterface.OnClickListener() { 208 public void onClick(DialogInterface dialog, int which) { 209 Uri uri = Uri.parse(linkUrls.get(which)); 210 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 211 intent.putExtra(Browser.EXTRA_APPLICATION_ID, mScreen.getPackageName()); 212 mScreen.startActivity(intent); 213 } 214 }); 215 b.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 216 public void onClick(DialogInterface dialog, int which) { 217 dialog.dismiss(); 218 } 219 }); 220 b.show(); 221 } 222 }; 223 224 private IChatListener mChatListener = new ChatListenerAdapter() { 225 @Override 226 public void onIncomingMessage(IChatSession ses, 227 com.android.im.engine.Message msg) { 228 scheduleRequery(0); 229 } 230 231 @Override 232 public void onContactJoined(IChatSession ses, Contact contact) { 233 scheduleRequery(0); 234 } 235 236 @Override 237 public void onContactLeft(IChatSession ses, Contact contact) { 238 scheduleRequery(0); 239 } 240 241 @Override 242 public void onSendMessageError(IChatSession ses, 243 com.android.im.engine.Message msg, ImErrorInfo error) { 244 scheduleRequery(0); 245 } 246 }; 247 248 private Runnable mUpdateChatCallback = new Runnable() { 249 public void run() { 250 if (mCursor.requery() && mCursor.moveToFirst()) { 251 updateChat(); 252 } 253 } 254 }; 255 private IContactListListener mContactListListener = new IContactListListener.Stub () { 256 public void onAllContactListsLoaded() { 257 } 258 259 public void onContactChange(int type, IContactList list, Contact contact){ 260 } 261 262 public void onContactError(int errorType, ImErrorInfo error, 263 String listName, Contact contact) { 264 } 265 266 public void onContactsPresenceUpdate(Contact[] contacts) { 267 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) { 268 log("onContactsPresenceUpdate()"); 269 } 270 for (Contact c : contacts) { 271 if (c.getAddress().getFullName().equals(mUserName)) { 272 mHandler.post(mUpdateChatCallback); 273 scheduleRequery(0); 274 break; 275 } 276 } 277 } 278 }; 279 280 static final void log(String msg) { 281 Log.d(ImApp.LOG_TAG, "<ChatView> " +msg); 282 } 283 284 public ChatView(Context context, AttributeSet attrs) { 285 super(context, attrs); 286 mScreen = (Activity) context; 287 mApp = ImApp.getApplication(mScreen); 288 mHandler = new ChatViewHandler(); 289 } 290 291 void registerForConnEvents() { 292 mApp.registerForConnEvents(mHandler); 293 } 294 295 void unregisterForConnEvents() { 296 mApp.unregisterForConnEvents(mHandler); 297 } 298 299 @Override 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); 307 308 mStatusWarningView = findViewById(R.id.warning); 309 mWarningIcon = (ImageView)findViewById(R.id.warningIcon); 310 mWarningText = (TextView)findViewById(R.id.warningText); 311 312 Button acceptInvitation = (Button)findViewById(R.id.btnAccept); 313 Button declineInvitation= (Button)findViewById(R.id.btnDecline); 314 315 Button approveSubscription = (Button)findViewById(R.id.btnApproveSubscription); 316 Button declineSubscription = (Button)findViewById(R.id.btnDeclineSubscription); 317 318 acceptInvitation.setOnClickListener(new OnClickListener() { 319 public void onClick(View v) { 320 acceptInvitation(); 321 } 322 }); 323 declineInvitation.setOnClickListener(new OnClickListener() { 324 public void onClick(View v) { 325 declineInvitation(); 326 } 327 }); 328 329 approveSubscription.setOnClickListener(new OnClickListener(){ 330 public void onClick(View v) { 331 approveSubscription(); 332 } 333 }); 334 declineSubscription.setOnClickListener(new OnClickListener(){ 335 public void onClick(View v) { 336 declineSubscription(); 337 } 338 }); 339 340 mEdtInput.setOnKeyListener(new OnKeyListener(){ 341 public boolean onKey(View v, int keyCode, KeyEvent event) { 342 if (event.getAction() == KeyEvent.ACTION_DOWN) { 343 switch (keyCode) { 344 case KeyEvent.KEYCODE_DPAD_CENTER: 345 sendMessage(); 346 return true; 347 348 case KeyEvent.KEYCODE_ENTER: 349 if (event.isAltPressed()) { 350 mEdtInput.append("\n"); 351 return true; 352 } 353 } 354 } 355 return false; 356 } 357 }); 358 359 mEdtInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { 360 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 361 if (event != null) { 362 if (event.isAltPressed()) { 363 return false; 364 } 365 } 366 367 sendMessage(); 368 return true; 369 } 370 }); 371 372 // TODO: this is a hack to implement BUG #1611278, when dispatchKeyEvent() works with 373 // the soft keyboard, we should remove this hack. 374 mEdtInput.addTextChangedListener(new TextWatcher() { 375 public void beforeTextChanged(CharSequence s, int start, int before, int after) { 376 } 377 378 public void onTextChanged(CharSequence s, int start, int before, int after) { 379 //log("TextWatcher: " + s); 380 userActionDetected(); 381 } 382 383 public void afterTextChanged(Editable s) { 384 } 385 }); 386 387 mSendButton.setOnClickListener(new OnClickListener() { 388 public void onClick(View v) { 389 sendMessage(); 390 } 391 }); 392 } 393 394 public void onResume(){ 395 if (mViewType == VIEW_TYPE_CHAT) { 396 Cursor cursor = getMessageCursor(); 397 if (cursor == null) { 398 startQuery(); 399 } else { 400 requeryCursor(); 401 } 402 updateWarningView(); 403 } 404 registerChatListener(); 405 registerForConnEvents(); 406 } 407 408 public void onPause(){ 409 Cursor cursor = getMessageCursor(); 410 if (cursor != null) { 411 cursor.deactivate(); 412 } 413 cancelRequery(); 414 if (mViewType == VIEW_TYPE_CHAT && mChatSession != null) { 415 try { 416 mChatSession.markAsRead(); 417 } catch (RemoteException e) { 418 mHandler.showServiceErrorAlert(); 419 } 420 } 421 unregisterChatListener(); 422 unregisterForConnEvents(); 423 unregisterChatSessionListener(); 424 } 425 426 private void closeSoftKeyboard() { 427 InputMethodManager inputMethodManager = 428 (InputMethodManager)mApp.getSystemService(Context.INPUT_METHOD_SERVICE); 429 430 inputMethodManager.hideSoftInputFromWindow(mEdtInput.getWindowToken(), 0); 431 } 432 433 void updateChat() { 434 setViewType(VIEW_TYPE_CHAT); 435 436 long oldChatId = mChatId; 437 438 updateContactInfo(); 439 440 setStatusIcon(); 441 setTitle(); 442 443 IImConnection conn = mApp.getConnection(mProviderId); 444 if (conn == null) { 445 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)) log("Connection has been signed out"); 446 mScreen.finish(); 447 return; 448 } 449 450 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId); 451 mHistory.setBackgroundDrawable( 452 brandingRes.getDrawable(BrandingResourceIDs.DRAWABLE_CHAT_WATERMARK)); 453 454 if (mMarkup == null) { 455 mMarkup = new Markup(brandingRes); 456 } 457 458 if (mMessageAdapter == null) { 459 mMessageAdapter = new MessageAdapter(mScreen, null); 460 mHistory.setAdapter(mMessageAdapter); 461 } 462 463 // only change the message adapter when we switch to another chat 464 if (mChatId != oldChatId) { 465 startQuery(); 466 mEdtInput.setText(""); 467 } 468 469 updateWarningView(); 470 } 471 472 private void updateContactInfo() { 473 mChatId = mCursor.getLong(CONTACT_ID_COLUMN); 474 mProviderId = mCursor.getLong(PROVIDER_COLUMN); 475 mAccountId = mCursor.getLong(ACCOUNT_COLUMN); 476 mPresenceStatus = mCursor.getInt(PRESENCE_STATUS_COLUMN); 477 mType = mCursor.getInt(TYPE_COLUMN); 478 mUserName = mCursor.getString(USERNAME_COLUMN); 479 mNickName = mCursor.getString(NICKNAME_COLUMN); 480 } 481 482 private void setTitle() { 483 if (mType == Imps.Contacts.TYPE_GROUP) { 484 final String[] projection = {Imps.GroupMembers.NICKNAME}; 485 Uri memberUri = ContentUris.withAppendedId(Imps.GroupMembers.CONTENT_URI, mChatId); 486 ContentResolver cr = mScreen.getContentResolver(); 487 Cursor c = cr.query(memberUri, projection, null, null, null); 488 StringBuilder buf = new StringBuilder(); 489 if(c != null) { 490 while(c.moveToNext()) { 491 buf.append(c.getString(0)); 492 if(!c.isLast()) { 493 buf.append(','); 494 } 495 } 496 c.close(); 497 } 498 mTitle.setText(mContext.getString(R.string.chat_with, buf.toString())); 499 } else { 500 mTitle.setText(mContext.getString(R.string.chat_with, mNickName)); 501 } 502 } 503 504 private void setStatusIcon() { 505 if (mType == Imps.Contacts.TYPE_GROUP) { 506 // hide the status icon for group chat. 507 mStatusIcon.setVisibility(GONE); 508 } else { 509 mStatusIcon.setVisibility(VISIBLE); 510 BrandingResources brandingRes = mApp.getBrandingResource(mProviderId); 511 int presenceResId = PresenceUtils.getStatusIconId(mPresenceStatus); 512 mStatusIcon.setImageDrawable(brandingRes.getDrawable(presenceResId)); 513 } 514 } 515 516 public void bindChat(long chatId) { 517 if (mCursor != null) { 518 mCursor.deactivate(); 519 } 520 Uri contactUri = ContentUris.withAppendedId(Imps.Contacts.CONTENT_URI, chatId); 521 mCursor = mScreen.managedQuery(contactUri, CHAT_PROJECTION, null, null); 522 if (mCursor == null || !mCursor.moveToFirst()) { 523 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 524 log("Failed to query chat: " + chatId); 525 } 526 mScreen.finish(); 527 return; 528 } else { 529 mChatSession = getChatSession(mCursor); 530 updateChat(); 531 registerChatListener(); 532 } 533 } 534 535 public void bindInvitation(long invitationId) { 536 Uri uri = ContentUris.withAppendedId(Imps.Invitation.CONTENT_URI, invitationId); 537 ContentResolver cr = mScreen.getContentResolver(); 538 Cursor cursor = cr.query(uri, INVITATION_PROJECT, null, null, null); 539 if (cursor == null || !cursor.moveToFirst()) { 540 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 541 log("Failed to query invitation: " + invitationId); 542 } 543 mScreen.finish(); 544 } else { 545 setViewType(VIEW_TYPE_INVITATION); 546 547 mInvitationId = cursor.getLong(INVITATION_ID_COLUMN); 548 mProviderId = cursor.getLong(INVITATION_PROVIDER_COLUMN); 549 String sender = cursor.getString(INVITATION_SENDER_COLUMN); 550 551 TextView mInvitationText = (TextView)findViewById(R.id.txtInvitation); 552 mInvitationText.setText(mContext.getString(R.string.invitation_prompt, sender)); 553 mTitle.setText(mContext.getString(R.string.chat_with, sender)); 554 } 555 556 if (cursor != null) { 557 cursor.close(); 558 } 559 } 560 561 public void bindSubscription(long providerId, String from) { 562 mProviderId = providerId; 563 mUserName = from; 564 565 setViewType(VIEW_TYPE_SUBSCRIPTION); 566 567 TextView text = (TextView)findViewById(R.id.txtSubscription); 568 String displayableAddr = ImpsAddressUtils.getDisplayableAddress(from); 569 text.setText(mContext.getString(R.string.subscription_prompt, displayableAddr)); 570 mTitle.setText(mContext.getString(R.string.chat_with, displayableAddr)); 571 572 mApp.dismissChatNotification(providerId, from); 573 } 574 575 void acceptInvitation() { 576 try { 577 578 IImConnection conn = mApp.getConnection(mProviderId); 579 if (conn != null) { 580 // register a chat session listener and wait for a group chat 581 // session to be created after we accept the invitation. 582 registerChatSessionListener(); 583 conn.acceptInvitation(mInvitationId); 584 } 585 } catch (RemoteException e) { 586 mHandler.showServiceErrorAlert(); 587 } 588 } 589 590 void declineInvitation() { 591 try { 592 IImConnection conn = mApp.getConnection(mProviderId); 593 if (conn != null) { 594 conn.rejectInvitation(mInvitationId); 595 } 596 mScreen.finish(); 597 } catch (RemoteException e) { 598 mHandler.showServiceErrorAlert(); 599 } 600 } 601 602 void approveSubscription() { 603 IImConnection conn = mApp.getConnection(mProviderId); 604 try { 605 IContactListManager manager = conn.getContactListManager(); 606 manager.approveSubscription(mUserName); 607 } catch (RemoteException ex) { 608 mHandler.showServiceErrorAlert(); 609 } 610 mScreen.finish(); 611 } 612 613 void declineSubscription() { 614 IImConnection conn = mApp.getConnection(mProviderId); 615 try { 616 IContactListManager manager = conn.getContactListManager(); 617 manager.declineSubscription(mUserName); 618 } catch (RemoteException ex) { 619 mHandler.showServiceErrorAlert(); 620 } 621 mScreen.finish(); 622 } 623 624 private void setViewType(int type) { 625 mViewType = type; 626 if (type == VIEW_TYPE_CHAT) { 627 findViewById(R.id.invitationPanel).setVisibility(GONE); 628 findViewById(R.id.subscription).setVisibility(GONE); 629 setChatViewEnabled(true); 630 } else if (type == VIEW_TYPE_INVITATION) { 631 setChatViewEnabled(false); 632 findViewById(R.id.invitationPanel).setVisibility(VISIBLE); 633 findViewById(R.id.btnAccept).requestFocus(); 634 } else if (type == VIEW_TYPE_SUBSCRIPTION) { 635 setChatViewEnabled(false); 636 findViewById(R.id.subscription).setVisibility(VISIBLE); 637 findViewById(R.id.btnApproveSubscription).requestFocus(); 638 } 639 } 640 641 private void setChatViewEnabled(boolean enabled) { 642 mEdtInput.setEnabled(enabled); 643 mSendButton.setEnabled(enabled); 644 if (enabled) { 645 mEdtInput.requestFocus(); 646 } else { 647 mHistory.setAdapter(null); 648 } 649 } 650 651 private void startQuery() { 652 if (mQueryHandler == null) { 653 mQueryHandler = new QueryHandler(mContext); 654 } else { 655 // Cancel any pending queries 656 mQueryHandler.cancelOperation(QUERY_TOKEN); 657 } 658 659 Uri uri = Imps.Messages.getContentUriByThreadId(mChatId); 660 661 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 662 log("queryCursor: uri=" + uri); 663 } 664 665 mQueryHandler.startQuery(QUERY_TOKEN, null, 666 uri, 667 null, 668 null /* selection */, 669 null /* selection args */, 670 null); 671 } 672 673 void scheduleRequery(long interval) { 674 if (mRequeryCallback == null) { 675 mRequeryCallback = new RequeryCallback(); 676 } else { 677 mHandler.removeCallbacks(mRequeryCallback); 678 } 679 680 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 681 log("scheduleRequery"); 682 } 683 mHandler.postDelayed(mRequeryCallback, interval); 684 } 685 686 void cancelRequery() { 687 if (mRequeryCallback != null) { 688 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 689 log("cancelRequery"); 690 } 691 mHandler.removeCallbacks(mRequeryCallback); 692 mRequeryCallback = null; 693 } 694 } 695 696 void requeryCursor() { 697 if (mMessageAdapter.isScrolling()) { 698 mMessageAdapter.setNeedRequeryCursor(true); 699 return; 700 } 701 // TODO: async query? 702 Cursor cursor = getMessageCursor(); 703 if (cursor != null) { 704 cursor.requery(); 705 } 706 } 707 708 private Cursor getMessageCursor() { 709 return mMessageAdapter == null ? null : mMessageAdapter.getCursor(); 710 } 711 712 public void insertSmiley(String smiley) { 713 mEdtInput.append(mMarkup.applyEmoticons(smiley)); 714 } 715 716 public void closeChatSession() { 717 if (mChatSession != null) { 718 try { 719 mChatSession.leave(); 720 } catch (RemoteException e) { 721 mHandler.showServiceErrorAlert(); 722 } 723 } else { 724 // the conversation is already closed, clear data in database 725 ContentResolver cr = mContext.getContentResolver(); 726 cr.delete(ContentUris.withAppendedId(Imps.Chats.CONTENT_URI, mChatId), 727 null, null); 728 } 729 mScreen.finish(); 730 } 731 732 public void closeChatSessionIfInactive() { 733 if (mChatSession != null) { 734 try { 735 mChatSession.leaveIfInactive(); 736 } catch (RemoteException e) { 737 mHandler.showServiceErrorAlert(); 738 } 739 } 740 } 741 742 public void viewProfile() { 743 Uri data = ContentUris.withAppendedId(Imps.Contacts.CONTENT_URI, mChatId); 744 Intent intent = new Intent(Intent.ACTION_VIEW, data); 745 mScreen.startActivity(intent); 746 } 747 748 public void blockContact() { 749 // TODO: unify with codes in ContactListView 750 DialogInterface.OnClickListener confirmListener = new DialogInterface.OnClickListener(){ 751 public void onClick(DialogInterface dialog, int whichButton) { 752 try { 753 IImConnection conn = mApp.getConnection(mProviderId); 754 IContactListManager manager = conn.getContactListManager(); 755 manager.blockContact(mUserName); 756 mScreen.finish(); 757 } catch (RemoteException e) { 758 mHandler.showServiceErrorAlert(); 759 } 760 } 761 }; 762 763 Resources r = getResources(); 764 765 // The positive button is deliberately set as no so that 766 // the no is the default value 767 new AlertDialog.Builder(mContext) 768 .setTitle(R.string.confirm) 769 .setMessage(r.getString(R.string.confirm_block_contact, mNickName)) 770 .setPositiveButton(R.string.yes, confirmListener) // default button 771 .setNegativeButton(R.string.no, null) 772 .setCancelable(false) 773 .show(); 774 } 775 776 public long getProviderId() { 777 return mProviderId; 778 } 779 780 public long getAccountId() { 781 return mAccountId; 782 } 783 784 public String getUserName() { 785 return mUserName; 786 } 787 788 public long getChatId () { 789 try { 790 return mChatSession == null ? -1 : mChatSession.getId(); 791 } catch (RemoteException e) { 792 mHandler.showServiceErrorAlert(); 793 return -1; 794 } 795 } 796 797 public IChatSession getCurrentChatSession() { 798 return mChatSession; 799 } 800 801 private IChatSessionManager getChatSessionManager(long providerId) { 802 if (mChatSessionMgr == null) { 803 IImConnection conn = mApp.getConnection(providerId); 804 if (conn != null) { 805 try { 806 mChatSessionMgr = conn.getChatSessionManager(); 807 } catch (RemoteException e) { 808 mHandler.showServiceErrorAlert(); 809 } 810 } 811 } 812 return mChatSessionMgr; 813 } 814 815 private IChatSession getChatSession(Cursor cursor) { 816 long providerId = cursor.getLong(PROVIDER_COLUMN); 817 String username = cursor.getString(USERNAME_COLUMN); 818 819 IChatSessionManager sessionMgr = getChatSessionManager(providerId); 820 if (sessionMgr != null) { 821 try { 822 return sessionMgr.getChatSession(username); 823 } catch (RemoteException e) { 824 mHandler.showServiceErrorAlert(); 825 } 826 } 827 return null; 828 } 829 830 boolean isGroupChat() { 831 return Imps.Contacts.TYPE_GROUP == mType; 832 } 833 834 void sendMessage() { 835 String msg = mEdtInput.getText().toString(); 836 837 if (TextUtils.isEmpty(msg.trim())) { 838 return; 839 } 840 841 if (mChatSession != null) { 842 try { 843 mChatSession.sendMessage(msg); 844 mEdtInput.setText(""); 845 mEdtInput.requestFocus(); 846 requeryCursor(); 847 } catch (RemoteException e) { 848 mHandler.showServiceErrorAlert(); 849 } 850 } 851 852 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 853 // conversation. 854 Configuration config = getResources().getConfiguration(); 855 if (config.orientation == config.ORIENTATION_LANDSCAPE) { 856 closeSoftKeyboard(); 857 } 858 } 859 860 void registerChatListener() { 861 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 862 log("registerChatListener"); 863 } 864 try { 865 if (mChatSession != null) { 866 mChatSession.registerChatListener(mChatListener); 867 } 868 IImConnection conn = mApp.getConnection(mProviderId); 869 if (conn != null) { 870 IContactListManager listMgr = conn.getContactListManager(); 871 listMgr.registerContactListListener(mContactListListener); 872 } 873 mApp.dismissChatNotification(mProviderId, mUserName); 874 } catch (RemoteException e) { 875 Log.w(ImApp.LOG_TAG, "<ChatView> registerChatListener fail:" + e.getMessage()); 876 } 877 } 878 879 void unregisterChatListener() { 880 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 881 log("unregisterChatListener"); 882 } 883 try { 884 if (mChatSession != null) { 885 mChatSession.unregisterChatListener(mChatListener); 886 } 887 IImConnection conn = mApp.getConnection(mProviderId); 888 if (conn != null) { 889 IContactListManager listMgr = conn.getContactListManager(); 890 listMgr.unregisterContactListListener(mContactListListener); 891 } 892 } catch (RemoteException e) { 893 Log.w(ImApp.LOG_TAG, "<ChatView> unregisterChatListener fail:" + e.getMessage()); 894 } 895 } 896 897 void registerChatSessionListener() { 898 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId); 899 if (sessionMgr != null) { 900 mChatSessionListener = new ChatSessionListener(); 901 try { 902 sessionMgr.registerChatSessionListener(mChatSessionListener); 903 } catch (RemoteException e) { 904 mHandler.showServiceErrorAlert(); 905 } 906 } 907 } 908 909 void unregisterChatSessionListener() { 910 if (mChatSessionListener != null) { 911 try { 912 IChatSessionManager sessionMgr = getChatSessionManager(mProviderId); 913 sessionMgr.unregisterChatSessionListener(mChatSessionListener); 914 // We unregister the listener when the chat session we are 915 // waiting for has been created or the activity is stopped. 916 // Clear the listener so that we won't unregister the listener 917 // twice. 918 mChatSessionListener = null; 919 } catch (RemoteException e) { 920 mHandler.showServiceErrorAlert(); 921 } 922 } 923 } 924 925 void updateWarningView() { 926 int visibility = View.GONE; 927 int iconVisibility = View.GONE; 928 String message = null; 929 boolean isConnected; 930 931 try { 932 IImConnection conn = mApp.getConnection(mProviderId); 933 isConnected = (conn == null) ? false 934 : conn.getState() != ImConnection.SUSPENDED; 935 } catch (RemoteException e) { 936 // do nothing 937 return; 938 } 939 940 if (isConnected) { 941 if (mType == Imps.Contacts.TYPE_TEMPORARY) { 942 visibility = View.VISIBLE; 943 message = mContext.getString(R.string.contact_not_in_list_warning, mNickName); 944 } else if (mPresenceStatus == Imps.Presence.OFFLINE) { 945 visibility = View.VISIBLE; 946 message = mContext.getString(R.string.contact_offline_warning, mNickName); 947 } 948 } else { 949 visibility = View.VISIBLE; 950 iconVisibility = View.VISIBLE; 951 message = mContext.getString(R.string.disconnected_warning); 952 } 953 954 mStatusWarningView.setVisibility(visibility); 955 if (visibility == View.VISIBLE) { 956 mWarningIcon.setVisibility(iconVisibility); 957 mWarningText.setText(message); 958 } 959 } 960 961 @Override 962 public boolean dispatchKeyEvent(KeyEvent event) { 963 userActionDetected(); 964 return super.dispatchKeyEvent(event); 965 } 966 967 @Override 968 public boolean dispatchTouchEvent(MotionEvent ev) { 969 userActionDetected(); 970 return super.dispatchTouchEvent(ev); 971 } 972 973 @Override 974 public boolean dispatchTrackballEvent(MotionEvent ev) { 975 userActionDetected(); 976 return super.dispatchTrackballEvent(ev); 977 } 978 979 private void userActionDetected() { 980 if (mChatSession != null) { 981 try { 982 mChatSession.markAsRead(); 983 } catch (RemoteException e) { 984 mHandler.showServiceErrorAlert(); 985 } 986 } 987 } 988 989 private final class ChatViewHandler extends SimpleAlertHandler { 990 public ChatViewHandler() { 991 super(mScreen); 992 } 993 994 @Override 995 public void handleMessage(Message msg) { 996 long providerId = ((long)msg.arg1 << 32) | msg.arg2; 997 if (providerId != mProviderId) { 998 return; 999 } 1000 1001 switch(msg.what) { 1002 case ImApp.EVENT_CONNECTION_LOGGED_IN: 1003 log("Connection resumed"); 1004 updateWarningView(); 1005 return; 1006 case ImApp.EVENT_CONNECTION_SUSPENDED: 1007 log("Connection suspended"); 1008 updateWarningView(); 1009 return; 1010 } 1011 1012 super.handleMessage(msg); 1013 } 1014 } 1015 1016 class ChatSessionListener extends ChatSessionListenerAdapter { 1017 @Override 1018 public void onChatSessionCreated(IChatSession session) { 1019 try { 1020 if (session.isGroupChatSession()) { 1021 final long id = session.getId(); 1022 unregisterChatSessionListener(); 1023 mHandler.post(new Runnable() { 1024 public void run() { 1025 bindChat(id); 1026 }}); 1027 } 1028 } catch (RemoteException e) { 1029 mHandler.showServiceErrorAlert(); 1030 } 1031 } 1032 } 1033 1034 public static class DeltaCursor implements Cursor { 1035 static final String DELTA_COLUMN_NAME = "delta"; 1036 1037 private Cursor mInnerCursor; 1038 private String[] mColumnNames; 1039 private int mDateColumn = -1; 1040 private int mDeltaColumn = -1; 1041 1042 DeltaCursor(Cursor cursor) { 1043 mInnerCursor = cursor; 1044 1045 String[] columnNames = cursor.getColumnNames(); 1046 int len = columnNames.length; 1047 1048 mColumnNames = new String[len + 1]; 1049 1050 for (int i = 0 ; i < len ; i++) { 1051 mColumnNames[i] = columnNames[i]; 1052 if (mColumnNames[i].equals(Imps.Messages.DATE)) { 1053 mDateColumn = i; 1054 } 1055 } 1056 1057 mDeltaColumn = len; 1058 mColumnNames[mDeltaColumn] = DELTA_COLUMN_NAME; 1059 1060 //if (DBG) log("##### DeltaCursor constructor: mDeltaColumn=" + 1061 // mDeltaColumn + ", columnName=" + mColumnNames[mDeltaColumn]); 1062 } 1063 1064 public int getCount() { 1065 return mInnerCursor.getCount(); 1066 } 1067 1068 public int getPosition() { 1069 return mInnerCursor.getPosition(); 1070 } 1071 1072 public boolean move(int offset) { 1073 return mInnerCursor.move(offset); 1074 } 1075 1076 public boolean moveToPosition(int position) { 1077 return mInnerCursor.moveToPosition(position); 1078 } 1079 1080 public boolean moveToFirst() { 1081 return mInnerCursor.moveToFirst(); 1082 } 1083 1084 public boolean moveToLast() { 1085 return mInnerCursor.moveToLast(); 1086 } 1087 1088 public boolean moveToNext() { 1089 return mInnerCursor.moveToNext(); 1090 } 1091 1092 public boolean moveToPrevious() { 1093 return mInnerCursor.moveToPrevious(); 1094 } 1095 1096 public boolean isFirst() { 1097 return mInnerCursor.isFirst(); 1098 } 1099 1100 public boolean isLast() { 1101 return mInnerCursor.isLast(); 1102 } 1103 1104 public boolean isBeforeFirst() { 1105 return mInnerCursor.isBeforeFirst(); 1106 } 1107 1108 public boolean isAfterLast() { 1109 return mInnerCursor.isAfterLast(); 1110 } 1111 1112 public boolean deleteRow() { 1113 return mInnerCursor.deleteRow(); 1114 } 1115 1116 public int getColumnIndex(String columnName) { 1117 if (DELTA_COLUMN_NAME.equals(columnName)) { 1118 return mDeltaColumn; 1119 } 1120 1121 int columnIndex = mInnerCursor.getColumnIndex(columnName); 1122 return columnIndex; 1123 } 1124 1125 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { 1126 if (DELTA_COLUMN_NAME.equals(columnName)) { 1127 return mDeltaColumn; 1128 } 1129 1130 return mInnerCursor.getColumnIndexOrThrow(columnName); 1131 } 1132 1133 public String getColumnName(int columnIndex) { 1134 if (columnIndex == mDeltaColumn) { 1135 return DELTA_COLUMN_NAME; 1136 } 1137 1138 return mInnerCursor.getColumnName(columnIndex); 1139 } 1140 1141 public int getColumnCount() { 1142 return mInnerCursor.getColumnCount() + 1; 1143 } 1144 1145 public boolean supportsUpdates() { 1146 return mInnerCursor.supportsUpdates(); 1147 } 1148 1149 public boolean hasUpdates() { 1150 return mInnerCursor.hasUpdates(); 1151 } 1152 1153 public boolean updateBlob(int columnIndex, byte[] value) { 1154 if (columnIndex == mDeltaColumn) { 1155 return false; 1156 } 1157 1158 return mInnerCursor.updateBlob(columnIndex, value); 1159 } 1160 1161 public boolean updateString(int columnIndex, String value) { 1162 if (columnIndex == mDeltaColumn) { 1163 return false; 1164 } 1165 1166 return mInnerCursor.updateString(columnIndex, value); 1167 } 1168 1169 public boolean updateShort(int columnIndex, short value) { 1170 if (columnIndex == mDeltaColumn) { 1171 return false; 1172 } 1173 1174 return mInnerCursor.updateShort(columnIndex, value); 1175 } 1176 1177 public boolean updateInt(int columnIndex, int value) { 1178 if (columnIndex == mDeltaColumn) { 1179 return false; 1180 } 1181 1182 return mInnerCursor.updateInt(columnIndex, value); 1183 } 1184 1185 public boolean updateLong(int columnIndex, long value) { 1186 if (columnIndex == mDeltaColumn) { 1187 return false; 1188 } 1189 1190 return mInnerCursor.updateLong(columnIndex, value); 1191 } 1192 1193 public boolean updateFloat(int columnIndex, float value) { 1194 if (columnIndex == mDeltaColumn) { 1195 return false; 1196 } 1197 1198 return mInnerCursor.updateFloat(columnIndex, value); 1199 } 1200 1201 public boolean updateDouble(int columnIndex, double value) { 1202 if (columnIndex == mDeltaColumn) { 1203 return false; 1204 } 1205 1206 return mInnerCursor.updateDouble(columnIndex, value); 1207 } 1208 1209 public boolean updateToNull(int columnIndex) { 1210 if (columnIndex == mDeltaColumn) { 1211 return false; 1212 } 1213 1214 return mInnerCursor.updateToNull(columnIndex); 1215 } 1216 1217 public boolean commitUpdates() { 1218 return mInnerCursor.commitUpdates(); 1219 } 1220 1221 public boolean commitUpdates(Map<? extends Long, 1222 ? extends Map<String,Object>> values) { 1223 return mInnerCursor.commitUpdates(values); 1224 } 1225 1226 public void abortUpdates() { 1227 mInnerCursor.abortUpdates(); 1228 } 1229 1230 public void deactivate() { 1231 mInnerCursor.deactivate(); 1232 } 1233 1234 public boolean requery() { 1235 return mInnerCursor.requery(); 1236 } 1237 1238 public void close() { 1239 mInnerCursor.close(); 1240 } 1241 1242 public boolean isClosed() { 1243 return mInnerCursor.isClosed(); 1244 } 1245 1246 public void registerContentObserver(ContentObserver observer) { 1247 mInnerCursor.registerContentObserver(observer); 1248 } 1249 1250 public void unregisterContentObserver(ContentObserver observer) { 1251 mInnerCursor.unregisterContentObserver(observer); 1252 } 1253 1254 public void registerDataSetObserver(DataSetObserver observer) { 1255 mInnerCursor.registerDataSetObserver(observer); 1256 } 1257 1258 public void unregisterDataSetObserver(DataSetObserver observer) { 1259 mInnerCursor.unregisterDataSetObserver(observer); 1260 } 1261 1262 public void setNotificationUri(ContentResolver cr, Uri uri) { 1263 mInnerCursor.setNotificationUri(cr, uri); 1264 } 1265 1266 public boolean getWantsAllOnMoveCalls() { 1267 return mInnerCursor.getWantsAllOnMoveCalls(); 1268 } 1269 1270 public Bundle getExtras() { 1271 return mInnerCursor.getExtras(); 1272 } 1273 1274 public Bundle respond(Bundle extras) { 1275 return mInnerCursor.respond(extras); 1276 } 1277 1278 public String[] getColumnNames() { 1279 return mColumnNames; 1280 } 1281 1282 private void checkPosition() { 1283 int pos = mInnerCursor.getPosition(); 1284 int count = mInnerCursor.getCount(); 1285 1286 if (-1 == pos || count == pos) { 1287 throw new CursorIndexOutOfBoundsException(pos, count); 1288 } 1289 } 1290 1291 public byte[] getBlob(int column) { 1292 checkPosition(); 1293 1294 if (column == mDeltaColumn) { 1295 return null; 1296 } 1297 1298 return mInnerCursor.getBlob(column); 1299 } 1300 1301 public String getString(int column) { 1302 checkPosition(); 1303 1304 if (column == mDeltaColumn) { 1305 long value = getDeltaValue(); 1306 return Long.toString(value); 1307 } 1308 1309 return mInnerCursor.getString(column); 1310 } 1311 1312 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 1313 checkPosition(); 1314 1315 if (columnIndex == mDeltaColumn) { 1316 long value = getDeltaValue(); 1317 String strValue = Long.toString(value); 1318 int len = strValue.length(); 1319 char[] data = buffer.data; 1320 if (data == null || data.length < len) { 1321 buffer.data = strValue.toCharArray(); 1322 } else { 1323 strValue.getChars(0, len, data, 0); 1324 } 1325 buffer.sizeCopied = strValue.length(); 1326 } else { 1327 mInnerCursor.copyStringToBuffer(columnIndex, buffer); 1328 } 1329 } 1330 1331 public short getShort(int column) { 1332 checkPosition(); 1333 1334 if (column == mDeltaColumn) { 1335 return (short)getDeltaValue(); 1336 } 1337 1338 return mInnerCursor.getShort(column); 1339 } 1340 1341 public int getInt(int column) { 1342 checkPosition(); 1343 1344 if (column == mDeltaColumn) { 1345 return (int)getDeltaValue(); 1346 } 1347 1348 return mInnerCursor.getInt(column); 1349 } 1350 1351 public long getLong(int column) { 1352 //if (DBG) log("DeltaCursor.getLong: column=" + column + ", mDeltaColumn=" + mDeltaColumn); 1353 checkPosition(); 1354 1355 if (column == mDeltaColumn) { 1356 return getDeltaValue(); 1357 } 1358 1359 return mInnerCursor.getLong(column); 1360 } 1361 1362 public float getFloat(int column) { 1363 checkPosition(); 1364 1365 if (column == mDeltaColumn) { 1366 return getDeltaValue(); 1367 } 1368 1369 return mInnerCursor.getFloat(column); 1370 } 1371 1372 public double getDouble(int column) { 1373 checkPosition(); 1374 1375 if (column == mDeltaColumn) { 1376 return getDeltaValue(); 1377 } 1378 1379 return mInnerCursor.getDouble(column); 1380 } 1381 1382 public boolean isNull(int column) { 1383 checkPosition(); 1384 1385 if (column == mDeltaColumn) { 1386 return false; 1387 } 1388 1389 return mInnerCursor.isNull(column); 1390 } 1391 1392 private long getDeltaValue() { 1393 int pos = mInnerCursor.getPosition(); 1394 //Log.i(LOG_TAG, "getDeltaValue: mPos=" + mPos); 1395 1396 long t2, t1; 1397 1398 if (pos == getCount()-1) { 1399 t1 = mInnerCursor.getLong(mDateColumn); 1400 t2 = System.currentTimeMillis(); 1401 } else { 1402 mInnerCursor.moveToPosition(pos + 1); 1403 t2 = mInnerCursor.getLong(mDateColumn); 1404 mInnerCursor.moveToPosition(pos); 1405 t1 = mInnerCursor.getLong(mDateColumn); 1406 } 1407 1408 return t2 - t1; 1409 } 1410 } 1411 1412 private class MessageAdapter extends CursorAdapter implements AbsListView.OnScrollListener { 1413 private int mScrollState; 1414 private boolean mNeedRequeryCursor; 1415 1416 private int mNicknameColumn; 1417 private int mBodyColumn; 1418 private int mDateColumn; 1419 private int mTypeColumn; 1420 private int mErrCodeColumn; 1421 private int mDeltaColumn; 1422 private ChatBackgroundMaker mBgMaker; 1423 1424 private LayoutInflater mInflater; 1425 1426 public MessageAdapter(Activity context, Cursor c) { 1427 super(context, c, false); 1428 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1429 mBgMaker = new ChatBackgroundMaker(context); 1430 if (c != null) { 1431 resolveColumnIndex(c); 1432 } 1433 } 1434 1435 private void resolveColumnIndex(Cursor c) { 1436 mNicknameColumn = c.getColumnIndexOrThrow(Imps.Messages.NICKNAME); 1437 mBodyColumn = c.getColumnIndexOrThrow(Imps.Messages.BODY); 1438 mDateColumn = c.getColumnIndexOrThrow(Imps.Messages.DATE); 1439 mTypeColumn = c.getColumnIndexOrThrow(Imps.Messages.TYPE); 1440 mErrCodeColumn = c.getColumnIndexOrThrow(Imps.Messages.ERROR_CODE); 1441 mDeltaColumn = c.getColumnIndexOrThrow(DeltaCursor.DELTA_COLUMN_NAME); 1442 } 1443 1444 @Override 1445 public void changeCursor(Cursor cursor) { 1446 super.changeCursor(cursor); 1447 if (cursor != null) { 1448 resolveColumnIndex(cursor); 1449 } 1450 } 1451 1452 @Override 1453 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1454 return mInflater.inflate(R.layout.new_message_item, parent, false); 1455 } 1456 1457 @Override 1458 public void bindView(View view, Context context, Cursor cursor) { 1459 MessageView chatMsgView = (MessageView) view; 1460 1461 int type = cursor.getInt(mTypeColumn); 1462 String contact = isGroupChat() ? cursor.getString(mNicknameColumn) : mNickName; 1463 String body = cursor.getString(mBodyColumn); 1464 long delta = cursor.getLong(mDeltaColumn); 1465 boolean showTimeStamp = (delta > SHOW_TIME_STAMP_INTERVAL); 1466 Date date = showTimeStamp ? new Date(cursor.getLong(mDateColumn)) : null; 1467 1468 switch (type) { 1469 case Imps.MessageType.INCOMING: 1470 chatMsgView.bindIncomingMessage(contact, body, date, mMarkup, isScrolling()); 1471 break; 1472 1473 case Imps.MessageType.OUTGOING: 1474 case Imps.MessageType.POSTPONED: 1475 int errCode = cursor.getInt(mErrCodeColumn); 1476 if (errCode != 0) { 1477 chatMsgView.bindErrorMessage(errCode); 1478 } else { 1479 chatMsgView.bindOutgoingMessage(body, date, mMarkup, isScrolling()); 1480 } 1481 break; 1482 1483 default: 1484 chatMsgView.bindPresenceMessage(contact, type, isGroupChat(), isScrolling()); 1485 } 1486 if (!isScrolling()) { 1487 mBgMaker.setBackground(chatMsgView, contact, type); 1488 } 1489 1490 // if showTimeStamp is false for the latest message, then set a timer to query the 1491 // cursor again in a minute, so we can update the last message timestamp if no new 1492 // message is received 1493 if (cursor.getPosition() == cursor.getCount()-1) { 1494 if (Log.isLoggable(ImApp.LOG_TAG, Log.DEBUG)){ 1495 log("delta = " + delta + ", showTs=" + showTimeStamp); 1496 } 1497 if (!showTimeStamp) { 1498 scheduleRequery(SHOW_TIME_STAMP_INTERVAL); 1499 } else { 1500 cancelRequery(); 1501 } 1502 } 1503 } 1504 1505 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1506 int totalItemCount) { 1507 // do nothing 1508 } 1509 1510 public void onScrollStateChanged(AbsListView view, int scrollState) { 1511 int oldState = mScrollState; 1512 mScrollState = scrollState; 1513 1514 if (mChatSession != null) { 1515 try { 1516 mChatSession.markAsRead(); 1517 } catch (RemoteException e) { 1518 mHandler.showServiceErrorAlert(); 1519 } 1520 } 1521 1522 if (oldState == OnScrollListener.SCROLL_STATE_FLING) { 1523 if (mNeedRequeryCursor) { 1524 requeryCursor(); 1525 } else { 1526 notifyDataSetChanged(); 1527 } 1528 } 1529 } 1530 1531 boolean isScrolling() { 1532 return mScrollState == OnScrollListener.SCROLL_STATE_FLING; 1533 } 1534 1535 void setNeedRequeryCursor(boolean requeryCursor) { 1536 mNeedRequeryCursor = requeryCursor; 1537 } 1538 } 1539} 1540