Remove the ListSelectionManager / message body selection (fixes memory leak!)
- When the `viewHolder.messageBody` `TextView` created by a `MessageAdapter` is set to selectable, it leaks an `android.widget.Editor` (because that editor registers a view observer that never gets unregistered). - This memory leak is really quite problematic, as the message adapter is used a lot! - Having the text be selectable is useless anyway, though; there isn't any way to select it (because long pressing just opens the context menu anyway). - It looks like the ListSelectionManager was meant to track selections across multiple messages. However, I'm not sure this feature ever gets used. - Accordingly, this commit removes the entire feature, thus fixing the memory leak (since no `Editor` objects are ever created). - It should also reduce memory usage in general, since we aren't attaching an `Editor` to every single textview we create. - A `TextView` only allocates an `Editor` if you ask it to do certain things, like make the text selectable or register custom selection callbacks.
This commit is contained in:
parent
afffe01868
commit
b4805ac2c5
|
@ -66,7 +66,6 @@ import eu.siacs.conversations.ui.util.MyLinkify;
|
||||||
import eu.siacs.conversations.ui.util.ViewUtil;
|
import eu.siacs.conversations.ui.util.ViewUtil;
|
||||||
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
|
||||||
import eu.siacs.conversations.ui.widget.CopyTextView;
|
import eu.siacs.conversations.ui.widget.CopyTextView;
|
||||||
import eu.siacs.conversations.ui.widget.ListSelectionManager;
|
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.utils.EmojiWrapper;
|
import eu.siacs.conversations.utils.EmojiWrapper;
|
||||||
import eu.siacs.conversations.utils.Emoticons;
|
import eu.siacs.conversations.utils.Emoticons;
|
||||||
|
@ -87,7 +86,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
|
||||||
private static final int DATE_SEPARATOR = 3;
|
private static final int DATE_SEPARATOR = 3;
|
||||||
private static final int RTP_SESSION = 4;
|
private static final int RTP_SESSION = 4;
|
||||||
private final XmppActivity activity;
|
private final XmppActivity activity;
|
||||||
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
|
|
||||||
private final AudioPlayer audioPlayer;
|
private final AudioPlayer audioPlayer;
|
||||||
private List<String> highlightedTerm = null;
|
private List<String> highlightedTerm = null;
|
||||||
private DisplayMetrics metrics;
|
private DisplayMetrics metrics;
|
||||||
|
@ -503,9 +501,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
|
||||||
MyLinkify.addLinks(body, true);
|
MyLinkify.addLinks(body, true);
|
||||||
viewHolder.messageBody.setAutoLinkMask(0);
|
viewHolder.messageBody.setAutoLinkMask(0);
|
||||||
viewHolder.messageBody.setText(EmojiWrapper.transform(body));
|
viewHolder.messageBody.setText(EmojiWrapper.transform(body));
|
||||||
viewHolder.messageBody.setTextIsSelectable(true);
|
|
||||||
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
|
viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
|
||||||
listSelectionManager.onUpdate(viewHolder.messageBody, message);
|
|
||||||
} else {
|
} else {
|
||||||
viewHolder.messageBody.setText("");
|
viewHolder.messageBody.setText("");
|
||||||
viewHolder.messageBody.setTextIsSelectable(false);
|
viewHolder.messageBody.setTextIsSelectable(false);
|
||||||
|
@ -676,8 +672,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
|
||||||
throw new AssertionError("Unknown view type");
|
throw new AssertionError("Unknown view type");
|
||||||
}
|
}
|
||||||
if (viewHolder.messageBody != null) {
|
if (viewHolder.messageBody != null) {
|
||||||
listSelectionManager.onCreate(viewHolder.messageBody,
|
|
||||||
new MessageBodyActionModeCallback(viewHolder.messageBody));
|
|
||||||
viewHolder.messageBody.setCopyHandler(this);
|
viewHolder.messageBody.setCopyHandler(this);
|
||||||
}
|
}
|
||||||
view.setTag(viewHolder);
|
view.setTag(viewHolder);
|
||||||
|
@ -875,13 +869,6 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
|
||||||
activity.showInstallPgpDialog();
|
activity.showInstallPgpDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDataSetChanged() {
|
|
||||||
listSelectionManager.onBeforeNotifyDataSetChanged();
|
|
||||||
super.notifyDataSetChanged();
|
|
||||||
listSelectionManager.onAfterNotifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String transformText(CharSequence text, int start, int end, boolean forCopy) {
|
private String transformText(CharSequence text, int start, int end, boolean forCopy) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
SpannableStringBuilder builder = new SpannableStringBuilder(text);
|
||||||
Object copySpan = new Object();
|
Object copySpan = new Object();
|
||||||
|
|
|
@ -1,211 +0,0 @@
|
||||||
package eu.siacs.conversations.ui.widget;
|
|
||||||
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.text.Selection;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.view.ActionMode;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
public class ListSelectionManager {
|
|
||||||
|
|
||||||
private static final int MESSAGE_SEND_RESET = 1;
|
|
||||||
private static final int MESSAGE_RESET = 2;
|
|
||||||
private static final int MESSAGE_START_SELECTION = 3;
|
|
||||||
private static final Field FIELD_EDITOR;
|
|
||||||
private static final Method METHOD_START_SELECTION;
|
|
||||||
private static final boolean SUPPORTED;
|
|
||||||
private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handleMessage(Message msg) {
|
|
||||||
switch (msg.what) {
|
|
||||||
case MESSAGE_SEND_RESET: {
|
|
||||||
// Skip one more message queue loop
|
|
||||||
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MESSAGE_RESET: {
|
|
||||||
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
|
|
||||||
listSelectionManager.futureSelectionIdentifier = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MESSAGE_START_SELECTION: {
|
|
||||||
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
|
|
||||||
holder.listSelectionManager.futureSelectionIdentifier = null;
|
|
||||||
startSelection(holder.textView, holder.start, holder.end);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
static {
|
|
||||||
Field editor;
|
|
||||||
try {
|
|
||||||
editor = TextView.class.getDeclaredField("mEditor");
|
|
||||||
editor.setAccessible(true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
editor = null;
|
|
||||||
}
|
|
||||||
FIELD_EDITOR = editor;
|
|
||||||
Method startSelection = null;
|
|
||||||
if (editor != null) {
|
|
||||||
String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
|
|
||||||
for (String startSelectionName : startSelectionNames) {
|
|
||||||
try {
|
|
||||||
startSelection = editor.getType().getDeclaredMethod(startSelectionName);
|
|
||||||
startSelection.setAccessible(true);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
startSelection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
METHOD_START_SELECTION = startSelection;
|
|
||||||
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ActionMode selectionActionMode;
|
|
||||||
private Object selectionIdentifier;
|
|
||||||
private TextView selectionTextView;
|
|
||||||
private Object futureSelectionIdentifier;
|
|
||||||
private int futureSelectionStart;
|
|
||||||
private int futureSelectionEnd;
|
|
||||||
|
|
||||||
public static boolean isSupported() {
|
|
||||||
return SUPPORTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void startSelection(TextView textView, int start, int end) {
|
|
||||||
final CharSequence text = textView.getText();
|
|
||||||
if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
|
|
||||||
final Spannable spannable = (Spannable) text;
|
|
||||||
start = Math.min(start, spannable.length());
|
|
||||||
end = Math.min(end, spannable.length());
|
|
||||||
Selection.setSelection(spannable, start, end);
|
|
||||||
try {
|
|
||||||
final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
|
|
||||||
METHOD_START_SELECTION.invoke(editor);
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
|
|
||||||
final CustomCallback callback = new CustomCallback(textView, additionalCallback);
|
|
||||||
textView.setCustomSelectionActionModeCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdate(TextView textView, Object identifier) {
|
|
||||||
if (SUPPORTED) {
|
|
||||||
final ActionMode.Callback callback = textView.getCustomSelectionActionModeCallback();
|
|
||||||
if (callback instanceof CustomCallback) {
|
|
||||||
final CustomCallback customCallback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
|
|
||||||
customCallback.identifier = identifier;
|
|
||||||
if (futureSelectionIdentifier == identifier) {
|
|
||||||
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
|
|
||||||
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onBeforeNotifyDataSetChanged() {
|
|
||||||
if (SUPPORTED) {
|
|
||||||
HANDLER.removeMessages(MESSAGE_SEND_RESET);
|
|
||||||
HANDLER.removeMessages(MESSAGE_RESET);
|
|
||||||
HANDLER.removeMessages(MESSAGE_START_SELECTION);
|
|
||||||
if (selectionActionMode != null) {
|
|
||||||
final CharSequence text = selectionTextView.getText();
|
|
||||||
futureSelectionIdentifier = selectionIdentifier;
|
|
||||||
futureSelectionStart = Selection.getSelectionStart(text);
|
|
||||||
futureSelectionEnd = Selection.getSelectionEnd(text);
|
|
||||||
selectionActionMode.finish();
|
|
||||||
selectionActionMode = null;
|
|
||||||
selectionIdentifier = null;
|
|
||||||
selectionTextView = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onAfterNotifyDataSetChanged() {
|
|
||||||
if (SUPPORTED && futureSelectionIdentifier != null) {
|
|
||||||
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class StartSelectionHolder {
|
|
||||||
|
|
||||||
final ListSelectionManager listSelectionManager;
|
|
||||||
final TextView textView;
|
|
||||||
public final int start;
|
|
||||||
public final int end;
|
|
||||||
|
|
||||||
StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
|
|
||||||
int start, int end) {
|
|
||||||
this.listSelectionManager = listSelectionManager;
|
|
||||||
this.textView = textView;
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CustomCallback implements ActionMode.Callback {
|
|
||||||
|
|
||||||
private final TextView textView;
|
|
||||||
private final ActionMode.Callback additionalCallback;
|
|
||||||
Object identifier;
|
|
||||||
|
|
||||||
CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
|
|
||||||
this.textView = textView;
|
|
||||||
this.additionalCallback = additionalCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
||||||
selectionActionMode = mode;
|
|
||||||
selectionIdentifier = identifier;
|
|
||||||
selectionTextView = textView;
|
|
||||||
if (additionalCallback != null) {
|
|
||||||
additionalCallback.onCreateActionMode(mode, menu);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
|
||||||
if (additionalCallback != null) {
|
|
||||||
additionalCallback.onPrepareActionMode(mode, menu);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
|
||||||
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyActionMode(ActionMode mode) {
|
|
||||||
if (additionalCallback != null) {
|
|
||||||
additionalCallback.onDestroyActionMode(mode);
|
|
||||||
}
|
|
||||||
if (selectionActionMode == mode) {
|
|
||||||
selectionActionMode = null;
|
|
||||||
selectionIdentifier = null;
|
|
||||||
selectionTextView = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue