save scroll state across rotations

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java |   2 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 147 
src/main/java/eu/siacs/conversations/ui/util/ScrollState.java     |  72 
3 files changed, 155 insertions(+), 66 deletions(-)

Detailed changes

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));
 		}
 	}
 

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<Message> messageList = new ArrayList<>();
@@ -134,6 +135,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 	private final PendingItem<String> pendingConversationsUuid = new PendingItem<>();
 	private final PendingItem<Bundle> pendingExtras = new PendingItem<>();
 	private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
+	private final PendingItem<ScrollState> 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<Message> messages) {
 		if (uuid == null) {
 			return messages.size() - 1;
@@ -429,28 +464,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
 		return -1;
 	}
 
-	public Pair<Integer, Integer> 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<Integer, Integer> 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() {

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<ScrollState> CREATOR = new Creator<ScrollState>() {
+		@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);
+	}
+}