From 1895b0198b36316e1dddb1ff6400a30b91a306a2 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Wed, 30 Mar 2022 12:31:58 +0200 Subject: [PATCH] Split long payload into pages to avoid UI freezes Rendering long text in TextViews makes the UI freeze. The payload is now paginated so that only the currently shown portion is shown in TextViews. --- .../adapters/PayloadAdapter.java | 218 ++++++++++++++---- .../fragments/ConnectionPayload.java | 5 - 2 files changed, 176 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java index 277c0d8a..4f028d06 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/PayloadAdapter.java @@ -27,7 +27,6 @@ import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.collection.ArraySet; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; @@ -47,16 +46,20 @@ import java.util.ArrayList; import java.util.Date; import java.util.Locale; +/* An adapter to show PayloadChunk items. + * Each item is wrapped into an AdapterChunk. An item can either be collapsed or expanded. + * Since the text of a chunk can be very long (hundreds of KB) and rendering it would freeze the UI, + * it is split into pages of VISUAL_PAGE_SIZE. */ public class PayloadAdapter extends RecyclerView.Adapter implements HTTPReassembly.ReassemblyListener { public static final int COLLAPSE_CHUNK_SIZE = 1500; + public static final int VISUAL_PAGE_SIZE = 4020; // must be a multiple of 67 to avoid splitting the hexdump private final LayoutInflater mLayoutInflater; private final ConnectionDescriptor mConn; private final Context mContext; private final Direction mDir; private int mHandledChunks; - private final ArrayList mChunks = new ArrayList<>(); + private final ArrayList mChunks = new ArrayList<>(); private final HTTPReassembly mHttp; - private final ArraySet mExpandedPos = new ArraySet<>(); public PayloadAdapter(Context context, ConnectionDescriptor conn, Direction dir) { mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -69,6 +72,111 @@ public class PayloadAdapter extends RecyclerView.Adapter COLLAPSE_CHUNK_SIZE; + } + + boolean isExpanded() { + return mIsExpanded; + } + + int getNumPages() { + return mNumPages; + } + + PayloadChunk getPayloadChunk() { + return mChunk; + } + + private void makeText() { + int dump_len = mIsExpanded ? mChunk.payload.length : Math.min(mChunk.payload.length, COLLAPSE_CHUNK_SIZE); + + if(isPayloadTab()) + mTheText = Utils.hexdump(mChunk.payload, 0, dump_len); + else + mTheText = new String(mChunk.payload, 0, dump_len, StandardCharsets.UTF_8); + } + + void expand() { + assert(!mIsExpanded); + + mIsExpanded = true; + makeText(); + + // round up div + mNumPages = (mTheText.length() + VISUAL_PAGE_SIZE - 1) / VISUAL_PAGE_SIZE; + } + + // collapses the item and returns the old number of pages + void collapse() { + assert(mIsExpanded); + + mIsExpanded = false; + mTheText = null; + + mNumPages = 1; + } + + String getText(int start, int end) { + if(mTheText == null) + makeText(); + + if((start == 0) && (end >= mTheText.length() - 1)) { + return mTheText; + } + + return mTheText.substring(start, end); + } + + Page getPage(int pageIdx) { + assert(pageIdx < mNumPages); + + if(mTheText == null) + makeText(); + + if(!mIsExpanded) + return new Page(this, 0, mTheText.length() - 1, true); + else + return new Page(this, pageIdx * VISUAL_PAGE_SIZE, + Math.min(((pageIdx + 1) * VISUAL_PAGE_SIZE) - 1, mTheText.length() - 1), + pageIdx == (mNumPages - 1)); + } + } + + private static class Page { + AdapterChunk adaptChunk; + int textStart; + int textEnd; + boolean isLast; + + Page(AdapterChunk _adaptChunk, int _textStart, int _textEnd, boolean _isLast) { + adaptChunk = _adaptChunk; + textStart = _textStart; + textEnd = _textEnd; + isLast = _isLast; + } + + boolean isFirst() { + return (textStart == 0); + } + + String getText() { + return adaptChunk.getText(textStart, textEnd); + } + } + protected static class PayloadViewHolder extends RecyclerView.ViewHolder { TextView header; TextView dump; @@ -91,11 +199,16 @@ public class PayloadAdapter extends RecyclerView.Adapter { int pos = holder.getAbsoluteAdapterPosition(); + Page page = getItem(pos); - if(mExpandedPos.contains(pos)) - mExpandedPos.remove(pos); - else - mExpandedPos.add(pos); + if(page.adaptChunk.isExpanded()) { + int numPages = page.adaptChunk.getNumPages(); + page.adaptChunk.collapse(); + notifyItemRangeRemoved(pos - (numPages - 1), numPages - 1); + } else { + page.adaptChunk.expand(); + notifyItemRangeInserted(pos + 1, page.adaptChunk.getNumPages() - 1); + } notifyItemChanged(pos); }); @@ -105,42 +218,38 @@ public class PayloadAdapter extends RecyclerView.Adapter COLLAPSE_CHUNK_SIZE) { + if(page.isLast && page.adaptChunk.canBeExpanded()) { holder.expandButton.setVisibility(View.VISIBLE); - holder.expandButton.setRotation(is_expanded ? 180 : 0); + holder.expandButton.setRotation(page.adaptChunk.isExpanded() ? 180 : 0); } else holder.expandButton.setVisibility(View.GONE); - int dump_len = is_expanded ? chunk.payload.length : Math.min(chunk.payload.length, COLLAPSE_CHUNK_SIZE); - String dump; - - if(isPayloadTab()) - dump = Utils.hexdump(chunk.payload, 0, dump_len); - else - dump = new String(chunk.payload, 0, dump_len, StandardCharsets.UTF_8); - - holder.dump.setText(dump); + holder.dump.setText(page.getText()); if(chunk.is_sent) { holder.dump.setBackgroundResource(R.color.sentPayloadBg); @@ -153,14 +262,37 @@ public class PayloadAdapter extends RecyclerView.Adapter mChunks.size())) + public Page getItem(int pos) { + if(pos < 0) return null; - return mChunks.get(pos); + int count = 0; + int i; + + // Find the AdapterChunk for the given page pos + for(i=0; i < mChunks.size(); i++) { + AdapterChunk aChunk = mChunks.get(i); + int new_count = count + aChunk.getNumPages(); + + if((pos >= count) && (pos < new_count)) + break; + + count = new_count; + } + + if(i >= mChunks.size()) + return null; + + int pageIdx = pos - count; + return mChunks.get(i).getPage(pageIdx); } private boolean isPayloadTab() { @@ -177,8 +309,9 @@ public class PayloadAdapter extends RecyclerView.Adapter