diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java
index f62cbdfc..f9ca0ccf 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/ConnectionDetailsActivity.java
@@ -14,11 +14,14 @@
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see .
*
- * Copyright 2020-21 - Emanuele Faranda
+ * Copyright 2020-24 - Emanuele Faranda
*/
package com.emanuelef.remote_capture.activities;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -26,6 +29,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import android.annotation.SuppressLint;
+import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -36,6 +40,8 @@ import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.ConnectionsRegister;
import com.emanuelef.remote_capture.Log;
import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.Utils;
+import com.emanuelef.remote_capture.adapters.PayloadAdapter;
import com.emanuelef.remote_capture.fragments.ConnectionOverview;
import com.emanuelef.remote_capture.fragments.ConnectionPayload;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
@@ -44,9 +50,12 @@ import com.emanuelef.remote_capture.model.PayloadChunk;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.util.ArrayList;
-public class ConnectionDetailsActivity extends BaseActivity implements ConnectionsListener {
+public class ConnectionDetailsActivity extends BaseActivity implements ConnectionsListener, PayloadAdapter.ExportPayloadHandler {
private static final String TAG = "ConnectionDetails";
public static final String CONN_ID_KEY = "conn_id";
private static final int MAX_CHUNKS_TO_CHECK = 10;
@@ -60,6 +69,7 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio
private boolean mHasPayload;
private boolean mHasHttpTab;
private boolean mHasWsTab;
+ private String mPayloadToExport;
private final ArrayList mListeners = new ArrayList<>();
private static final int POS_OVERVIEW = 0;
@@ -67,6 +77,9 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio
private static final int POS_HTTP = 2;
private static final int POS_RAW_PAYLOAD = 3;
+ private final ActivityResultLauncher payloadExportLauncher =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), this::payloadExportResult);
+
public interface ConnUpdateListener {
void connectionUpdated();
}
@@ -329,4 +342,38 @@ public class ConnectionDetailsActivity extends BaseActivity implements Connectio
return super.onKeyDown(keyCode, event);
}
+
+ @Override
+ public void exportPayload(String payload) {
+ mPayloadToExport = payload;
+
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TITLE, Utils.getUniqueFileName(this, "txt"));
+
+ Log.d(TAG, "exportPayload: launching dialog");
+ Utils.launchFileDialog(this, intent, payloadExportLauncher);
+ }
+
+ private void payloadExportResult(final ActivityResult result) {
+ Log.d(TAG, "payloadExportResult");
+
+ if (mPayloadToExport == null)
+ return;
+
+ if((result.getResultCode() == RESULT_OK) && (result.getData() != null) && (result.getData().getData() != null)) {
+ try(OutputStream out = getContentResolver().openOutputStream(result.getData().getData(), "rwt")) {
+ try(OutputStreamWriter writer = new OutputStreamWriter(out)) {
+ writer.write(mPayloadToExport);
+ }
+ Utils.showToast(this, R.string.save_ok);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Utils.showToastLong(this, R.string.export_failed);
+ }
+ }
+
+ mPayloadToExport = null;
+ }
}
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 8c66fe67..b633c14d 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
@@ -66,7 +66,13 @@ public class PayloadAdapter extends RecyclerView.Adapter mChunks = new ArrayList<>();
private final HTTPReassembly mHttpReq;
private final HTTPReassembly mHttpRes;
+ private final boolean mSupportsFileDialog;
private boolean mShowAsPrintable;
+ private ExportPayloadHandler mExportHandler;
+
+ public interface ExportPayloadHandler {
+ void exportPayload(String payload);
+ }
public PayloadAdapter(Context context, ConnectionDescriptor conn, ChunkType mode, boolean showAsPrintable) {
mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -74,6 +80,7 @@ public class PayloadAdapter extends RecyclerView.Adapter {
- int payload_pos = holder.getAbsoluteAdapterPosition();
-
- if(mMode == ChunkType.HTTP) {
- String payload = getItem(payload_pos).adaptChunk.getExpandedText(true);
- int crlf_pos = payload.indexOf("\r\n\r\n");
-
- boolean has_body = (crlf_pos > 0) && (crlf_pos < (payload.length() - 4));
- if (!has_body) {
- Utils.copyToClipboard(mContext, payload);
- return;
- }
-
- String[] choices = {
- mContext.getString(R.string.headers),
- mContext.getString(R.string.body),
- mContext.getString(R.string.both),
- };
-
- AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- builder.setTitle(R.string.copy_action);
- builder.setSingleChoiceItems(choices, 2, (dialogInterface, i) -> {});
- builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {});
- builder.setPositiveButton(R.string.copy_to_clipboard, (dialogInterface, i) -> {
- int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition();
- String to_copy = payload;
-
- if (choice != 2) {
- if (choice == 0 /* Headers */)
- to_copy = to_copy.substring(0, crlf_pos);
- else /* body */
- to_copy = to_copy.substring(crlf_pos + 4);
- }
-
- Utils.copyToClipboard(mContext, to_copy);
- });
- builder.create().show();
- } else {
- String[] choices = {
- mContext.getString(R.string.printable_text),
- mContext.getString(R.string.hexdump)
- };
-
- AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
- builder.setTitle(R.string.copy_action);
- builder.setSingleChoiceItems(choices, mShowAsPrintable ? 0 : 1, (dialogInterface, i) -> {});
-
- builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {});
- builder.setPositiveButton(R.string.copy_to_clipboard, (dialogInterface, i) -> {
- int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition();
- String payload = getItem(payload_pos).adaptChunk.getExpandedText(choice == 0);
-
- Utils.copyToClipboard(mContext, payload);
- });
- builder.create().show();
- }
- });
+ holder.copybutton.setOnClickListener(v -> handleCopyExportButtons(holder, false));
+ holder.exportbutton.setOnClickListener(v -> handleCopyExportButtons(holder, true));
+ holder.exportbutton.setVisibility(mSupportsFileDialog ? View.VISIBLE : View.GONE);
return holder;
}
+ private void handleCopyExportButtons(PayloadViewHolder holder, boolean is_export) {
+ if(is_export && (mExportHandler == null))
+ return;
+
+ int payload_pos = holder.getAbsoluteAdapterPosition();
+ int title = is_export ? R.string.export_ellipsis : R.string.copy_action;
+ int positive_action = is_export ? R.string.export_action : R.string.copy_to_clipboard;
+
+ if(mMode == ChunkType.HTTP) {
+ String payload = getItem(payload_pos).adaptChunk.getExpandedText(true);
+ int crlf_pos = payload.indexOf("\r\n\r\n");
+
+ boolean has_body = (crlf_pos > 0) && (crlf_pos < (payload.length() - 4));
+ if (!has_body) {
+ Utils.copyToClipboard(mContext, payload);
+ return;
+ }
+
+ String[] choices = {
+ mContext.getString(R.string.headers),
+ mContext.getString(R.string.body),
+ mContext.getString(R.string.both),
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(title);
+ builder.setSingleChoiceItems(choices, 2, (dialogInterface, i) -> {});
+ builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {});
+ builder.setPositiveButton(positive_action, (dialogInterface, i) -> {
+ int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition();
+ String to_copy = payload;
+
+ if (choice != 2) {
+ if (choice == 0 /* Headers */)
+ to_copy = to_copy.substring(0, crlf_pos);
+ else /* body */
+ to_copy = to_copy.substring(crlf_pos + 4);
+ }
+
+ if (is_export) {
+ if (mExportHandler != null)
+ mExportHandler.exportPayload(to_copy);
+ } else
+ Utils.copyToClipboard(mContext, to_copy);
+ });
+ builder.create().show();
+ } else {
+ String[] choices = {
+ mContext.getString(R.string.printable_text),
+ mContext.getString(R.string.hexdump)
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setTitle(title);
+ builder.setSingleChoiceItems(choices, mShowAsPrintable ? 0 : 1, (dialogInterface, i) -> {});
+
+ builder.setNeutralButton(R.string.cancel_action, (dialogInterface, i) -> {});
+ builder.setPositiveButton(positive_action, (dialogInterface, i) -> {
+ int choice = ((AlertDialog)dialogInterface).getListView().getCheckedItemPosition();
+ String payload = getItem(payload_pos).adaptChunk.getExpandedText(choice == 0);
+
+ if (is_export) {
+ if (mExportHandler != null)
+ mExportHandler.exportPayload(payload);
+ } else
+ Utils.copyToClipboard(mContext, payload);
+ });
+ builder.create().show();
+ }
+ }
+
private String getHeaderTag(PayloadChunk chunk) {
if(mMode == ChunkType.HTTP)
return (chunk.is_sent) ? mContext.getString(R.string.request) : mContext.getString(R.string.response);
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java
index f3ce0a26..d8bf595c 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionPayload.java
@@ -76,6 +76,9 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
super.onAttach(context);
mActivity = (ConnectionDetailsActivity) context;
mActivity.addConnUpdateListener(this);
+
+ if (mAdapter != null)
+ mAdapter.setExportPayloadHandler(mActivity);
}
@Override
@@ -83,6 +86,9 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
super.onDetach();
mActivity.removeConnUpdateListener(this);
mActivity = null;
+
+ if (mAdapter != null)
+ mAdapter.setExportPayloadHandler(null);
}
@Override
@@ -123,6 +129,7 @@ public class ConnectionPayload extends Fragment implements ConnectionDetailsActi
else
mShowAsPrintable = false;
mAdapter = new PayloadAdapter(requireContext(), mConn, mode, mShowAsPrintable);
+ mAdapter.setExportPayloadHandler(mActivity);
mJustCreated = true;
// only set adapter after acknowledged (see setMenuVisibility below)
diff --git a/app/src/main/res/drawable/ic_save_alt_small.xml b/app/src/main/res/drawable/ic_save_alt_small.xml
new file mode 100644
index 00000000..6d7013df
--- /dev/null
+++ b/app/src/main/res/drawable/ic_save_alt_small.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/payload_item.xml b/app/src/main/res/layout/payload_item.xml
index 147216a2..e131a769 100644
--- a/app/src/main/res/layout/payload_item.xml
+++ b/app/src/main/res/layout/payload_item.xml
@@ -49,6 +49,25 @@
app:iconPadding="0dp"
app:iconTint="@color/colorTabText"
style="@style/Widget.MaterialComponents.Button.TextButton" />
+
+
Mitm setup wizard
Install
Export
+ Export…
Install the PCAPdroid mitm addon
Configure
Export the PCAPdroid CA certificate, then open the Android \"Encryption & Credentials\" settings and choose install it as a \"CA certificate\"