From 79c585144d0967a2e5d4b1cfb6c486332d0b0cfb Mon Sep 17 00:00:00 2001 From: steckbrief Date: Wed, 12 Aug 2015 23:01:37 +0200 Subject: - Introduction of SwipeRefreshLayout for conversation view - OnScrollListener for ListView of messages removed and replaced with swipe refresh - Loading of messages now possible even if no message is present in conversation --- .../services/XmppConnectionService.java | 75 ++++---- .../conversations/ui/ConversationFragment.java | 191 ++++++--------------- .../ConversationMoreMessagesLoadedListener.java | 118 +++++++++++++ .../ConversationSwipeRefreshListener.java | 48 ++++++ src/main/res/layout/fragment_conversation.xml | 8 + 5 files changed, 271 insertions(+), 169 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java create mode 100644 src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 27bc502a..f50b9fa6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -382,21 +382,21 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } else { mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - getFileBackend().copyFileToPrivateStorage(message, uri); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); + @Override + public void run() { + try { + getFileBackend().copyFileToPrivateStorage(message, uri); + getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.FileCopyException e) { + callback.error(e.getResId(), message); + } + } + }); } } @@ -851,7 +851,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion()); - sendIqPacket(account,iqPacket,mIqParser); + sendIqPacket(account, iqPacket, mIqParser); } public void fetchBookmarks(final Account account) { @@ -1029,24 +1029,25 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } Collections.sort(list, new Comparator() { - @Override - public int compare(Conversation lhs, Conversation rhs) { - Message left = lhs.getLatestMessage(); - Message right = rhs.getLatestMessage(); - if (left.getTimeSent() > right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() < right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { + Log.d("mam", "Query in progress"); return; } Runnable runnable = new Runnable() { @@ -1054,19 +1055,26 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void run() { final Account account = conversation.getAccount(); List messages = databaseBackend.getMessages(conversation, 50,timestamp); + Log.d("mam", "runnable load more messages"); if (messages.size() > 0) { + Log.d("mam", "At least one message"); conversation.addAll(0, messages); checkDeletedFiles(conversation); callback.onMoreMessagesLoaded(messages.size(), conversation); } else if (conversation.hasMessagesLeftOnServer() && account.isOnlineAndConnected() && account.getXmppConnection().getFeatures().mam()) { + Log.d("mam", "mam activate, account online and connected and messages left on server"); MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1); if (query != null) { query.setCallback(callback); } callback.informUser(R.string.fetching_history_from_server); - } + } else { + Log.d("mam", ((!conversation.hasMessagesLeftOnServer()) ? "no" : "") + " more messages left on server, mam " + ((account.getXmppConnection().getFeatures().mam()) ? "" : "not") + " activated, account is " + ((account.isOnlineAndConnected()) ? "" : "not") + " online or connected)"); + callback.onMoreMessagesLoaded(0, conversation); + callback.informUser(R.string.no_more_history_on_server); + } } }; mDatabaseExecutor.execute(runnable); @@ -2496,7 +2504,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void clearConversationHistory(final Conversation conversation) { conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + /* + * In case the history was loaded completely before. + * The flag "hasMessagesLeftOnServer" is set to false and no messages will be loaded anymore + * Therefore set this flag to true and try to get messages from server + */ + conversation.setHasMessagesLeftOnServer(true); new Thread(new Runnable() { @Override public void run() { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 5a672520..dc472360 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -10,7 +10,9 @@ import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; import android.text.InputType; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; @@ -22,8 +24,6 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ImageButton; @@ -42,7 +42,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; -import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -61,11 +60,11 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.ui.listeners.ConversationSwipeRefreshListener; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; -import github.ankushsachdeva.emojicon.EmojiconEditText; import github.ankushsachdeva.emojicon.EmojiconGridView; import github.ankushsachdeva.emojicon.EmojiconsPopup; import github.ankushsachdeva.emojicon.emoji.Emojicon; @@ -107,7 +106,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } }; protected ListView messagesView; - final protected List messageList = new ArrayList<>(); + protected SwipeRefreshLayout swipeLayout; + final protected List messageList = new ArrayList<>(); protected MessageAdapter messageListAdapter; private EditMessage mEditMessage; private ImageButton mSendButton; @@ -117,96 +117,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private RelativeLayout snackbar; private TextView snackbarMessage; private TextView snackbarAction; - private boolean messagesLoaded = true; - private Toast messageLoaderToast; - - private OnScrollListener mOnScrollListener = new OnScrollListener() { - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // TODO Auto-generated method stub - - } - - private int getIndexOf(String uuid, List messages) { - if (uuid == null) { - return 0; - } - for(int i = 0; i < messages.size(); ++i) { - if (uuid.equals(messages.get(i).getUuid())) { - return i; - } else { - Message next = messages.get(i); - while(next != null && next.wasMergedIntoPrevious()) { - if (uuid.equals(next.getUuid())) { - return i; - } - next = next.next(); - } - - } - } - return 0; - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { - long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); - messagesLoaded = false; - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int c, Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - return; - } - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - final int oldPosition = messagesView.getFirstVisiblePosition(); - Message message = messageList.get(oldPosition); - String uuid = message != null ? message.getUuid() : null; - View v = messagesView.getChildAt(0); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - updateStatusMessages(); - messageListAdapter.notifyDataSetChanged(); - int pos = getIndexOf(uuid,messageList); - messagesView.setSelectionFromTop(pos, pxOffset); - messagesLoaded = true; - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - } - }); - } - - @Override - public void informUser(final int resId) { - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); - messageLoaderToast.show(); - } - }); - - } - }); - - } - } - } - }; private IntentSender askForPassphraseIntent = null; protected OnClickListener clickToDecryptListener = new OnClickListener() { @@ -322,8 +232,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResourcepart())); + R.string.send_private_message_to, + conversation.getNextCounterpart().getResourcepart())); } else { switch (conversation.getNextEncryption(activity.forceEncryption())) { case Message.ENCRYPTION_NONE: @@ -382,66 +292,66 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa //Set on emojicon click listener mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { - @Override - public void onEmojiconClicked(Emojicon emojicon) { - mEditMessage.append(emojicon.getEmoji()); - } - }); + @Override + public void onEmojiconClicked(Emojicon emojicon) { + mEditMessage.append(emojicon.getEmoji()); + } + }); //Set on backspace click listener mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { - @Override - public void onEmojiconBackspaceClicked(View v) { - KeyEvent event = new KeyEvent( - 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); - mEditMessage.dispatchKeyEvent(event); - } - }); + @Override + public void onEmojiconBackspaceClicked(View v) { + KeyEvent event = new KeyEvent( + 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); + mEditMessage.dispatchKeyEvent(event); + } + }); //If the emoji popup is dismissed, change emojiButton to smiley icon mEmojPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { - @Override - public void onDismiss() { - changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley); - } - }); + @Override + public void onDismiss() { + changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley); + } + }); //If the text keyboard closes, also dismiss the emoji popup mEmojPopup.setOnSoftKeyboardOpenCloseListener(new EmojiconsPopup.OnSoftKeyboardOpenCloseListener() { - @Override - public void onKeyboardOpen(int keyBoardHeight) { + @Override + public void onKeyboardOpen(int keyBoardHeight) { - } + } - @Override - public void onKeyboardClose() { - if (mEmojPopup.isShowing()) - mEmojPopup.dismiss(); - } - }); + @Override + public void onKeyboardClose() { + if (mEmojPopup.isShowing()) + mEmojPopup.dismiss(); + } + }); //On emoji clicked, add it to edittext mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { - @Override - public void onEmojiconClicked(Emojicon emojicon) { - mEditMessage.append(emojicon.getEmoji()); - } - }); + @Override + public void onEmojiconClicked(Emojicon emojicon) { + mEditMessage.append(emojicon.getEmoji()); + } + }); //On backspace clicked, emulate the KEYCODE_DEL key event mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { - @Override - public void onEmojiconBackspaceClicked(View v) { - KeyEvent event = new KeyEvent( - 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); - mEditMessage.dispatchKeyEvent(event); - } - }); + @Override + public void onEmojiconBackspaceClicked(View v) { + KeyEvent event = new KeyEvent( + 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); + mEditMessage.dispatchKeyEvent(event); + } + }); // To toggle between text keyboard and emoji keyboard keyboard(Popup) mEmojButton.setOnClickListener(new OnClickListener() { @@ -486,7 +396,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); messagesView = (ListView) view.findViewById(R.id.messages_view); - messagesView.setOnScrollListener(mOnScrollListener); messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList); messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() { @@ -533,6 +442,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa registerForContextMenu(messagesView); + // Start of swipe refresh + // New Swipe refresh + swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_container); + swipeLayout.setOnRefreshListener(new ConversationSwipeRefreshListener(messageList, swipeLayout, this, messagesView, messageListAdapter)); + // End of swipe refresh + return view; } @@ -763,11 +678,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mEditMessage.setKeyboardListener(this); this.messagesView.setAdapter(messageListAdapter); updateMessages(); - this.messagesLoaded = true; int size = this.messageList.size(); if (size > 0) { messagesView.setSelection(size - 1); } + swipeLayout.setRefreshing(false); } private OnClickListener mUnblockClickListener = new OnClickListener() { @@ -1094,7 +1009,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); } - protected void updateStatusMessages() { + public void updateStatusMessages() { synchronized (this.messageList) { if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java new file mode 100644 index 00000000..2154f8d8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java @@ -0,0 +1,118 @@ +package eu.siacs.conversations.ui.listeners; + +import android.support.v4.widget.SwipeRefreshLayout; +import android.view.View; +import android.widget.ListView; +import android.widget.Toast; + +import java.util.List; + +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.adapter.MessageAdapter; + +/** + * This listener updates the UI when messages are loaded from the server. + */ +public class ConversationMoreMessagesLoadedListener implements XmppConnectionService.OnMoreMessagesLoaded { + private SwipeRefreshLayout swipeLayout; + private List messageList; + private ConversationFragment fragment; + private ListView messagesView; + private MessageAdapter messageListAdapter; + private Toast messageLoaderToast; + + public ConversationMoreMessagesLoadedListener(SwipeRefreshLayout swipeLayout, List messageList, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) { + this.swipeLayout = swipeLayout; + this.messageList = messageList; + this.fragment = fragment; + this.messagesView = messagesView; + this.messageListAdapter = messageListAdapter; + } + + @Override + public void onMoreMessagesLoaded(final int c, final Conversation conversation) { + ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + if (activity.getSelectedConversation() != conversation) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + swipeLayout.setRefreshing(false); + } + }); + return; + } + if (0 == c) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + swipeLayout.setRefreshing(false); + } + }); + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final int oldPosition = messagesView.getFirstVisiblePosition(); + int pos = 0; + View v = messagesView.getChildAt(0); + final int pxOffset = (v == null) ? 0 : v.getTop(); + if (-1 < oldPosition && messageList.size() > oldPosition) { + Message message = messageList.get(oldPosition); + String uuid = message != null ? message.getUuid() : null; + pos = getIndexOf(uuid, messageList); + } + conversation.populateWithMessages(messageList); + fragment.updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + messagesView.setSelectionFromTop(pos, pxOffset); + + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + swipeLayout.setRefreshing(false); + } + }); + } + + @Override + public void informUser(final int resId) { + final ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); + messageLoaderToast.show(); + } + }); + + } + + private int getIndexOf(String uuid, List messages) { + if (uuid == null) { + return 0; + } + for(int i = 0; i < messages.size(); ++i) { + if (uuid.equals(messages.get(i).getUuid())) { + return i; + } else { + Message next = messages.get(i); + while(next != null && next.wasMergedIntoPrevious()) { + if (uuid.equals(next.getUuid())) { + return i; + } + next = next.next(); + } + + } + } + return 0; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java new file mode 100644 index 00000000..09a8d730 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.ui.listeners; + +import android.support.v4.widget.SwipeRefreshLayout; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.Toast; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.adapter.MessageAdapter; + +/** + * This listener starts loading messages from the server. + */ +public class ConversationSwipeRefreshListener implements SwipeRefreshLayout.OnRefreshListener { + private List messageList; + private ConversationFragment fragment; + private ConversationMoreMessagesLoadedListener listener; + + public ConversationSwipeRefreshListener(List messageList, SwipeRefreshLayout swipeLayout, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) { + this.messageList = messageList; + this.fragment = fragment; + this.listener = new ConversationMoreMessagesLoadedListener(swipeLayout, messageList, fragment, messagesView, messageListAdapter); + } + + @Override + public void onRefresh() { + Log.d(Config.LOGTAG, "Refresh swipe container"); + synchronized (this.messageList) { + long timestamp; + if (messageList.isEmpty()) { + timestamp = System.currentTimeMillis(); + } else { + timestamp = this.messageList.get(0).getTimeSent(); + } + ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + activity.xmppConnectionService.loadMoreMessages(activity.getSelectedConversation(), timestamp, this.listener); + } + Log.d(Config.LOGTAG, "End Refresh swipe container"); + } +} diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index 3fd80aa2..321055e9 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -5,6 +5,13 @@ android:layout_height="match_parent" android:background="@color/grey200" > + +