From 1236c6a139846064c762f95d11f2d66eacd3d888 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 25 Feb 2018 16:57:59 +0100 Subject: [PATCH] save scroll state across rotations --- .../ui/ConversationActivity.java | 2 + .../ui/ConversationFragment.java | 147 ++++++++++-------- .../conversations/ui/util/ScrollState.java | 72 +++++++++ 3 files changed, 155 insertions(+), 66 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/util/ScrollState.java diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index e184e900a..0c92192dd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -502,6 +502,8 @@ public class ConversationActivity extends XmppActivity implements OnConversation public void onConversationRead(Conversation conversation) { if (!mActivityPaused && pendingViewIntent.peek() == null) { xmppConnectionService.sendReadMarker(conversation); + } else { + Log.d(Config.LOGTAG,"ignoring read callback. mActivityPaused="+Boolean.toString(mActivityPaused)); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 9321c4809..b65f4e0fe 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -88,6 +88,7 @@ import eu.siacs.conversations.ui.util.AttachmentTool; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PresenceSelector; +import eu.siacs.conversations.ui.util.ScrollState; import eu.siacs.conversations.ui.util.SendButtonAction; import eu.siacs.conversations.ui.util.SendButtonTool; import eu.siacs.conversations.ui.widget.EditMessage; @@ -126,7 +127,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid"; public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; - public static final String STATE_PHOTO_URI = ConversationFragment.class.getName()+".take_photo_uri"; + public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".take_photo_uri"; final protected List messageList = new ArrayList<>(); @@ -134,6 +135,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private final PendingItem pendingConversationsUuid = new PendingItem<>(); private final PendingItem pendingExtras = new PendingItem<>(); private final PendingItem pendingTakePhotoUri = new PendingItem<>(); + private final PendingItem pendingScrollState = new PendingItem<>(); public Uri mPendingEditorContent = null; protected MessageAdapter messageListAdapter; private Conversation conversation; @@ -408,6 +410,39 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private boolean firstWord = false; private Message mPendingDownloadableMessage; + public static void downloadFile(Activity activity, Message message) { + Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); + if (fragment != null && fragment instanceof ConversationFragment) { + ((ConversationFragment) fragment).startDownloadable(message); + return; + } + fragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment); + if (fragment != null && fragment instanceof ConversationFragment) { + ((ConversationFragment) fragment).startDownloadable(message); + } + } + + public static Conversation getConversation(Activity activity) { + return getConversation(activity, R.id.secondary_fragment); + } + + private static Conversation getConversation(Activity activity, @IdRes int res) { + final Fragment fragment = activity.getFragmentManager().findFragmentById(res); + if (fragment != null && fragment instanceof ConversationFragment) { + return ((ConversationFragment) fragment).getConversation(); + } else { + return null; + } + } + + public static Conversation getConversationReliable(Activity activity) { + final Conversation conversation = getConversation(activity, R.id.secondary_fragment); + if (conversation != null) { + return conversation; + } + return getConversation(activity, R.id.main_fragment); + } + private int getIndexOf(String uuid, List messages) { if (uuid == null) { return messages.size() - 1; @@ -429,28 +464,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return -1; } - public Pair getScrollPosition() { - if (this.binding.messagesView.getCount() == 0 || - this.binding.messagesView.getLastVisiblePosition() == this.binding.messagesView.getCount() - 1) { + private ScrollState getScrollPosition() { + final ListView listView = this.binding.messagesView; + if (listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) { return null; } else { - final int pos = this.binding.messagesView.getFirstVisiblePosition(); - final View view = this.binding.messagesView.getChildAt(0); + final int pos = listView.getFirstVisiblePosition(); + final View view = listView.getChildAt(0); if (view == null) { return null; } else { - return new Pair<>(pos, view.getTop()); + return new ScrollState(pos, view.getTop()); } } } - public void setScrollPosition(Pair scrollPosition) { + private void setScrollPosition(ScrollState scrollPosition) { if (scrollPosition != null) { - this.binding.messagesView.setSelectionFromTop(scrollPosition.first, scrollPosition.second); + this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset); } } - private void attachLocationToConversation(Conversation conversation, Uri uri) { if (conversation == null) { return; @@ -678,7 +712,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (takePhotoUri != null) { attachImageToConversation(conversation, takePhotoUri); } else { - Log.d(Config.LOGTAG,"lost take photo uri. unable to to attach"); + Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); } break; case ATTACHMENT_CHOICE_CHOOSE_FILE: @@ -750,7 +784,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke setHasOptionsMenu(true); } - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { menuInflater.inflate(R.menu.fragment_conversation, menu); @@ -1386,6 +1419,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke getActivity().invalidateOptionsMenu(); }); super.onResume(); + if (activity != null && this.conversation != null) { + activity.onConversationRead(this.conversation); + } } private void showErrorMessage(final Message message) { @@ -1486,18 +1522,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - public static void downloadFile(Activity activity, Message message) { - Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment != null && fragment instanceof ConversationFragment) { - ((ConversationFragment) fragment).startDownloadable(message); - return; - } - fragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment); - if (fragment != null && fragment instanceof ConversationFragment) { - ((ConversationFragment) fragment).startDownloadable(message); - } - } - private void cancelTransmission(Message message) { Transferable transferable = message.getTransferable(); if (transferable != null) { @@ -1563,16 +1587,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (conversation != null) { outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); - Uri uri = pendingTakePhotoUri.pop(); + final Uri uri = pendingTakePhotoUri.pop(); if (uri != null) { outState.putString(STATE_PHOTO_URI, uri.toString()); } + final ScrollState scrollState = getScrollPosition(); + if (scrollState != null) { + outState.putParcelable(STATE_SCROLL_POSITION, scrollState); + } } } @@ -1589,6 +1616,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (takePhotoUri != null) { pendingTakePhotoUri.push(Uri.parse(takePhotoUri)); } + pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION)); } } @@ -1602,7 +1630,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke processExtras(extras); } } else { - Log.d(Config.LOGTAG,"skipped reinit on start"); + Log.d(Config.LOGTAG, "skipped reinit on start"); } } @@ -1636,7 +1664,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (this.activity == null || this.binding == null || previousConversation == null) { return; } - Log.d(Config.LOGTAG,"ConversationFragment.saveMessageDraftStopAudioPlayer()"); + Log.d(Config.LOGTAG, "ConversationFragment.saveMessageDraftStopAudioPlayer()"); final String msg = this.binding.textinput.getText().toString(); if (previousConversation.setNextMessage(msg)) { activity.xmppConnectionService.updateConversation(previousConversation); @@ -1666,14 +1694,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (conversation == null) { return false; } + final boolean hasChanged = this.conversation != null && this.conversation != conversation; this.conversation = conversation; //once we set the conversation all is good and it will automatically do the right thing in onStart() if (this.activity == null || this.binding == null) { return false; } - Log.d(Config.LOGTAG, "reInit(restore="+Boolean.toString(restore)+")"); + Log.d(Config.LOGTAG, "reInit(restore=" + Boolean.toString(restore) + ", hasChanged=" + Boolean.toString(hasChanged) + ")"); setupIme(); - if (!restore) { + if (!restore && hasChanged) { this.conversation.trim(); } @@ -1683,22 +1712,24 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.binding.textinput.append(this.conversation.getNextMessage()); this.binding.textinput.setKeyboardListener(this); messageListAdapter.updatePreferences(); - this.binding.messagesView.setAdapter(messageListAdapter); + if (!restore && hasChanged) { + this.binding.messagesView.setAdapter(messageListAdapter); + } refresh(false); this.conversation.messagesLoaded.set(true); - final boolean isAtBottom; - synchronized (this.messageList) { - final Message first = conversation.getFirstUnreadMessage(); - final int bottom = Math.max(0, this.messageList.size() - 1); - final int pos; - if (first == null) { - pos = bottom; - } else { - int i = getIndexOf(first.getUuid(), this.messageList); - pos = i < 0 ? bottom : i; + if (!restore && hasChanged) { + synchronized (this.messageList) { + final Message first = conversation.getFirstUnreadMessage(); + final int bottom = Math.max(0, this.messageList.size() - 1); + final int pos; + if (first == null) { + pos = bottom; + } else { + int i = getIndexOf(first.getUuid(), this.messageList); + pos = i < 0 ? bottom : i; + } + this.binding.messagesView.setSelection(pos); } - this.binding.messagesView.setSelection(pos); - isAtBottom = pos == bottom; } activity.onConversationRead(this.conversation); @@ -1827,13 +1858,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void refresh() { if (this.binding == null) { - Log.d(Config.LOGTAG,"ConversationFragment.refresh() skipped updated because view binding was null"); + Log.d(Config.LOGTAG, "ConversationFragment.refresh() skipped updated because view binding was null"); return; } this.refresh(true); } - private void refresh(boolean notifyConversationRead) { synchronized (this.messageList) { if (this.conversation != null) { @@ -2295,6 +2325,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } reInit(conversation, true); + ScrollState scrollState = pendingScrollState.pop(); + if (scrollState != null) { + setScrollPosition(scrollState); + } } ActivityResult activityResult = postponedActivityResult.pop(); if (activityResult != null) { @@ -2306,27 +2340,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (postponedActivityResult.pop() != null) { Log.d(Config.LOGTAG, "cleared pending intent with unhandled result left"); } - } - - public static Conversation getConversation(Activity activity) { - return getConversation(activity, R.id.secondary_fragment); - } - - private static Conversation getConversation(Activity activity, @IdRes int res) { - final Fragment fragment = activity.getFragmentManager().findFragmentById(res); - if (fragment != null && fragment instanceof ConversationFragment) { - return ((ConversationFragment) fragment).getConversation(); - } else { - return null; - } - } - - public static Conversation getConversationReliable(Activity activity) { - final Conversation conversation = getConversation(activity, R.id.secondary_fragment); - if (conversation != null) { - return conversation; - } - return getConversation(activity, R.id.main_fragment); + pendingScrollState.pop(); + pendingTakePhotoUri.pop(); } public Conversation getConversation() { diff --git a/src/main/java/eu/siacs/conversations/ui/util/ScrollState.java b/src/main/java/eu/siacs/conversations/ui/util/ScrollState.java new file mode 100644 index 000000000..5a87533a3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/ScrollState.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.ui.util; + +import android.os.Parcel; +import android.os.Parcelable; + +public class ScrollState implements Parcelable { + + public final int position; + public final int offset; + + private ScrollState(Parcel in) { + position = in.readInt(); + offset = in.readInt(); + } + + public ScrollState(int position, int offset) { + this.position = position; + this.offset = offset; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ScrollState createFromParcel(Parcel in) { + return new ScrollState(in); + } + + @Override + public ScrollState[] newArray(int size) { + return new ScrollState[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(position); + dest.writeInt(offset); + } +}