save scroll state across rotations

This commit is contained in:
Daniel Gultsch 2018-02-25 16:57:59 +01:00
parent 21c9ffd8b2
commit 1236c6a139
3 changed files with 155 additions and 66 deletions

View file

@ -502,6 +502,8 @@ public class ConversationActivity extends XmppActivity implements OnConversation
public void onConversationRead(Conversation conversation) { public void onConversationRead(Conversation conversation) {
if (!mActivityPaused && pendingViewIntent.peek() == null) { if (!mActivityPaused && pendingViewIntent.peek() == null) {
xmppConnectionService.sendReadMarker(conversation); xmppConnectionService.sendReadMarker(conversation);
} else {
Log.d(Config.LOGTAG,"ignoring read callback. mActivityPaused="+Boolean.toString(mActivityPaused));
} }
} }

View file

@ -88,6 +88,7 @@ import eu.siacs.conversations.ui.util.AttachmentTool;
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.PresenceSelector; 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.SendButtonAction;
import eu.siacs.conversations.ui.util.SendButtonTool; import eu.siacs.conversations.ui.util.SendButtonTool;
import eu.siacs.conversations.ui.widget.EditMessage; import eu.siacs.conversations.ui.widget.EditMessage;
@ -134,6 +135,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private final PendingItem<String> pendingConversationsUuid = new PendingItem<>(); private final PendingItem<String> pendingConversationsUuid = new PendingItem<>();
private final PendingItem<Bundle> pendingExtras = new PendingItem<>(); private final PendingItem<Bundle> pendingExtras = new PendingItem<>();
private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>(); private final PendingItem<Uri> pendingTakePhotoUri = new PendingItem<>();
private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
public Uri mPendingEditorContent = null; public Uri mPendingEditorContent = null;
protected MessageAdapter messageListAdapter; protected MessageAdapter messageListAdapter;
private Conversation conversation; private Conversation conversation;
@ -408,6 +410,39 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private boolean firstWord = false; private boolean firstWord = false;
private Message mPendingDownloadableMessage; 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) { private int getIndexOf(String uuid, List<Message> messages) {
if (uuid == null) { if (uuid == null) {
return messages.size() - 1; return messages.size() - 1;
@ -429,28 +464,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return -1; return -1;
} }
public Pair<Integer, Integer> getScrollPosition() { private ScrollState getScrollPosition() {
if (this.binding.messagesView.getCount() == 0 || final ListView listView = this.binding.messagesView;
this.binding.messagesView.getLastVisiblePosition() == this.binding.messagesView.getCount() - 1) { if (listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) {
return null; return null;
} else { } else {
final int pos = this.binding.messagesView.getFirstVisiblePosition(); final int pos = listView.getFirstVisiblePosition();
final View view = this.binding.messagesView.getChildAt(0); final View view = listView.getChildAt(0);
if (view == null) { if (view == null) {
return null; return null;
} else { } 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) { 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) { private void attachLocationToConversation(Conversation conversation, Uri uri) {
if (conversation == null) { if (conversation == null) {
return; return;
@ -750,7 +784,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.fragment_conversation, menu); menuInflater.inflate(R.menu.fragment_conversation, menu);
@ -1386,6 +1419,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
getActivity().invalidateOptionsMenu(); getActivity().invalidateOptionsMenu();
}); });
super.onResume(); super.onResume();
if (activity != null && this.conversation != null) {
activity.onConversationRead(this.conversation);
}
} }
private void showErrorMessage(final Message message) { 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) { private void cancelTransmission(Message message) {
Transferable transferable = message.getTransferable(); Transferable transferable = message.getTransferable();
if (transferable != null) { if (transferable != null) {
@ -1563,16 +1587,19 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
if (conversation != null) { if (conversation != null) {
outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid());
Uri uri = pendingTakePhotoUri.pop(); final Uri uri = pendingTakePhotoUri.pop();
if (uri != null) { if (uri != null) {
outState.putString(STATE_PHOTO_URI, uri.toString()); 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) { if (takePhotoUri != null) {
pendingTakePhotoUri.push(Uri.parse(takePhotoUri)); pendingTakePhotoUri.push(Uri.parse(takePhotoUri));
} }
pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION));
} }
} }
@ -1666,14 +1694,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (conversation == null) { if (conversation == null) {
return false; return false;
} }
final boolean hasChanged = this.conversation != null && this.conversation != conversation;
this.conversation = conversation; this.conversation = conversation;
//once we set the conversation all is good and it will automatically do the right thing in onStart() //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) { if (this.activity == null || this.binding == null) {
return false; 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(); setupIme();
if (!restore) { if (!restore && hasChanged) {
this.conversation.trim(); this.conversation.trim();
} }
@ -1683,10 +1712,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
this.binding.textinput.append(this.conversation.getNextMessage()); this.binding.textinput.append(this.conversation.getNextMessage());
this.binding.textinput.setKeyboardListener(this); this.binding.textinput.setKeyboardListener(this);
messageListAdapter.updatePreferences(); messageListAdapter.updatePreferences();
if (!restore && hasChanged) {
this.binding.messagesView.setAdapter(messageListAdapter); this.binding.messagesView.setAdapter(messageListAdapter);
}
refresh(false); refresh(false);
this.conversation.messagesLoaded.set(true); this.conversation.messagesLoaded.set(true);
final boolean isAtBottom; if (!restore && hasChanged) {
synchronized (this.messageList) { synchronized (this.messageList) {
final Message first = conversation.getFirstUnreadMessage(); final Message first = conversation.getFirstUnreadMessage();
final int bottom = Math.max(0, this.messageList.size() - 1); final int bottom = Math.max(0, this.messageList.size() - 1);
@ -1698,7 +1729,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
pos = i < 0 ? bottom : i; pos = i < 0 ? bottom : i;
} }
this.binding.messagesView.setSelection(pos); this.binding.messagesView.setSelection(pos);
isAtBottom = pos == bottom; }
} }
activity.onConversationRead(this.conversation); activity.onConversationRead(this.conversation);
@ -1833,7 +1864,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
this.refresh(true); this.refresh(true);
} }
private void refresh(boolean notifyConversationRead) { private void refresh(boolean notifyConversationRead) {
synchronized (this.messageList) { synchronized (this.messageList) {
if (this.conversation != null) { if (this.conversation != null) {
@ -2295,6 +2325,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return; return;
} }
reInit(conversation, true); reInit(conversation, true);
ScrollState scrollState = pendingScrollState.pop();
if (scrollState != null) {
setScrollPosition(scrollState);
}
} }
ActivityResult activityResult = postponedActivityResult.pop(); ActivityResult activityResult = postponedActivityResult.pop();
if (activityResult != null) { if (activityResult != null) {
@ -2306,27 +2340,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
if (postponedActivityResult.pop() != null) { if (postponedActivityResult.pop() != null) {
Log.d(Config.LOGTAG, "cleared pending intent with unhandled result left"); Log.d(Config.LOGTAG, "cleared pending intent with unhandled result left");
} }
} pendingScrollState.pop();
pendingTakePhotoUri.pop();
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);
} }
public Conversation getConversation() { public Conversation getConversation() {

View file

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