User can now specify text/hexdump visualization

This commit is contained in:
emanuele-f 2022-04-27 17:12:11 +02:00
parent 6ffcdd41da
commit 38e24f4a1f
6 changed files with 129 additions and 5 deletions

View File

@ -1125,4 +1125,8 @@ public class Utils {
icon.setColorFilter(ContextCompat.getColor(context, color));
icon.setImageDrawable(ContextCompat.getDrawable(context, resid));
}
public static boolean isPrintable(byte c) {
return ((c >= 32) && (c <= 126)) || (c == '\r') || (c == '\n') || (c == '\t');
}
}

View File

@ -19,6 +19,7 @@
package com.emanuelef.remote_capture.adapters;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
@ -63,6 +64,7 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
private final ArrayList<AdapterChunk> mChunks = new ArrayList<>();
private final HTTPReassembly mHttpReq;
private final HTTPReassembly mHttpRes;
private boolean mShowAsPrintable;
public PayloadAdapter(Context context, ConnectionDescriptor conn, ChunkType mode) {
mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@ -111,15 +113,13 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
private void makeText() {
int dump_len = mIsExpanded ? mChunk.payload.length : Math.min(mChunk.payload.length, COLLAPSE_CHUNK_SIZE);
if(mMode == ChunkType.RAW)
if(!mShowAsPrintable)
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();
@ -129,8 +129,6 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
// collapses the item and returns the old number of pages
void collapse() {
assert(mIsExpanded);
mIsExpanded = false;
mTheText = null;
@ -307,6 +305,19 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
return mChunks.get(i).getPage(pageIdx);
}
@SuppressLint("NotifyDataSetChanged")
public void setDisplayAsPrintableText(boolean asText) {
if(mShowAsPrintable != asText) {
mShowAsPrintable = asText;
// Chunk pagination depends on the displayed data length, collapsing everything is simpler
// than handling individual changes
for(AdapterChunk chunk: mChunks)
chunk.collapse(); // resets the chunk text
notifyDataSetChanged();
}
}
private int getAdapterPosition(AdapterChunk chunk) {
int i;
int count = 0;

View File

@ -22,6 +22,9 @@ package com.emanuelef.remote_capture.fragments;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@ -31,6 +34,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.activities.ConnectionDetailsActivity;
import com.emanuelef.remote_capture.adapters.PayloadAdapter;
import com.emanuelef.remote_capture.model.ConnectionDescriptor;
@ -43,6 +47,9 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
private PayloadAdapter mAdapter;
private TextView mTruncatedWarning;
private int mCurChunks;
private Menu mMenu;
private boolean mJustCreated;
private boolean mShowAsPrintable;
public static ConnectionPayload newInstance(PayloadChunk.ChunkType mode) {
ConnectionPayload fragment = new ConnectionPayload();
@ -70,6 +77,7 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
setHasOptionsMenu(true);
return inflater.inflate(R.layout.connection_payload, container, false);
}
@ -94,10 +102,83 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
mAdapter = new PayloadAdapter(requireContext(), mConn, mode);
mCurChunks = mConn.getNumPayloadChunks();
recyclerView.setAdapter(mAdapter);
mJustCreated = true;
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.connection_payload, menu);
mMenu = menu;
if((mCurChunks > 0) && mJustCreated) {
mShowAsPrintable = guessDisplayAsPrintable();
mAdapter.setDisplayAsPrintableText(mShowAsPrintable);
mJustCreated = false;
}
refreshDisplayMode();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if(id == R.id.printable_text) {
mShowAsPrintable = true;
mAdapter.setDisplayAsPrintableText(true);
refreshDisplayMode();
return true;
} else if(id == R.id.hexdump) {
mShowAsPrintable = false;
mAdapter.setDisplayAsPrintableText(false);
refreshDisplayMode();
return true;
}
return super.onOptionsItemSelected(item);
}
private boolean guessDisplayAsPrintable() {
// try to determine the best mode based on the current payload
if(mConn.getNumPayloadChunks() == 0)
return mConn.l7proto.equals("HTTPS");
PayloadChunk firstChunk = mConn.getPayloadChunk(0);
if(firstChunk.type == PayloadChunk.ChunkType.HTTP)
return true;
// guess based on the actual data
int maxLen = Math.min(firstChunk.payload.length, 16);
for(int i=0; i<maxLen; i++) {
if(!Utils.isPrintable(firstChunk.payload[i]))
return false;
}
return true;
}
private void refreshDisplayMode() {
if(mMenu == null)
return;
MenuItem printableText = mMenu.findItem(R.id.printable_text);
MenuItem hexdump = mMenu.findItem(R.id.hexdump);
// important: the checked item must first be unchecked
if(mShowAsPrintable) {
hexdump.setChecked(false);
printableText.setChecked(true);
} else {
printableText.setChecked(false);
hexdump.setChecked(true);
}
}
@Override
public void connectionUpdated() {
if(mCurChunks == 0) {
mShowAsPrintable = guessDisplayAsPrintable();
mAdapter.setDisplayAsPrintableText(mShowAsPrintable);
}
if(mConn.getNumPayloadChunks() > mCurChunks) {
mAdapter.handleChunksAdded(mConn.getNumPayloadChunks());
mCurChunks = mConn.getNumPayloadChunks();

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4,9h16v2H4V9zM4,13h10v2H4V13z"/>
</vector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/display_as"
android:icon="@drawable/ic_short_text"
app:showAsAction="ifRoom">
<menu>
<group android:id="@+id/display_as_grp" android:checkableBehavior="single">
<item
android:id="@+id/printable_text"
android:title="@string/printable_text" />
<item
android:id="@+id/hexdump"
android:title="@string/hexdump" />
</group>
</menu>
</item>
</menu>

View File

@ -314,4 +314,7 @@
<string name="network_traffic">Traffic</string>
<string name="warn_no_app_data">No application data has been exchanged</string>
<string name="waiting_application_data">Waiting data</string>
<string name="display_as">Display as…</string>
<string name="printable_text">Printable text</string>
<string name="hexdump">Hexdump</string>
</resources>