From 746e22c90b7322ba50fcf590b6c425dc80fa040a Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Sun, 25 Jun 2023 22:40:03 +0200 Subject: [PATCH] Add ability to load addons from storage - Select a folder and load addons from it - View list of available addons, including Js Injector - Ability to turn addons on/off See #8 --- app/src/main/AndroidManifest.xml | 7 +- .../main/java/com/pcapdroid/mitm/Addon.java | 24 ++ .../com/pcapdroid/mitm/AddonsActivity.java | 284 ++++++++++++++++++ .../com/pcapdroid/mitm/AddonsAdapter.java | 92 ++++++ .../pcapdroid/mitm/JsInjectorActivity.java | 43 ++- .../java/com/pcapdroid/mitm/MainActivity.java | 6 +- .../java/com/pcapdroid/mitm/MitmService.java | 17 +- .../com/pcapdroid/mitm/ScriptsAdapter.java | 19 ++ .../main/java/com/pcapdroid/mitm/Utils.java | 45 +++ app/src/main/python/mitm.py | 43 ++- .../res/drawable/ic_baseline_settings.xml | 5 + app/src/main/res/layout/addon_item.xml | 45 +++ app/src/main/res/layout/main_activity.xml | 4 +- app/src/main/res/menu/addons.xml | 24 ++ app/src/main/res/values/strings.xml | 6 + 15 files changed, 630 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/pcapdroid/mitm/Addon.java create mode 100644 app/src/main/java/com/pcapdroid/mitm/AddonsActivity.java create mode 100644 app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java create mode 100644 app/src/main/java/com/pcapdroid/mitm/Utils.java create mode 100644 app/src/main/res/drawable/ic_baseline_settings.xml create mode 100644 app/src/main/res/layout/addon_item.xml create mode 100644 app/src/main/res/menu/addons.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f829c41..79b5dfb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,10 +39,15 @@ + + . + * + * Copyright 2023 - Emanuele Faranda + */ + +package com.pcapdroid.mitm; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.UriPermission; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.provider.DocumentsContract; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AddonsActivity extends Activity implements AddonsAdapter.AddonListener { + private static final String TAG = "UserAddons"; + private static final String USER_DIR_PREF = "user-dir"; + private static final String ENABLED_ADDONS_PREF = "enabled-addons"; + private static final int OPEN_DIR_TREE_CODE = 1; + + private TextView mEmptyText; + private SharedPreferences mPrefs; + private AddonsAdapter mAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.addons); + setContentView(R.layout.simple_list); + + mEmptyText = findViewById(R.id.list_empty); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + mAdapter = new AddonsAdapter(this); + mAdapter.setListener(this); + ((ListView)findViewById(R.id.listview)) + .setAdapter(mAdapter); + refreshAddons(); + } + + private void refreshAddons() { + Set enabledAddons = getEnabledAddons(this); + List addons = new ArrayList<>(); + + // Internal addons + addons.add(new Addon("Js Injector", + "Inject javascript into web pages", + enabledAddons.contains("Js Injector"), + Addon.AddonType.JsInjector)); + + // User addons + Uri publicUri = Uri.parse(mPrefs.getString(USER_DIR_PREF, "")); + String descr = getString(R.string.user_addon); + + if((publicUri.getHost() != null) && hasUriPersistablePermission(this, publicUri)) { + for (Uri uri : listUserAddons(this, publicUri)) { + String path = uri.getPath(); + int slash = path.lastIndexOf("/"); + if (slash > 0) { + String fname = path.substring(slash + 1); + if (fname.endsWith(".py")) { + String script = fname.substring(0, fname.length() - 3); + addons.add(new Addon(script, descr, enabledAddons.contains(script))); + } + } + } + } + + mAdapter.reload(addons); + recheckListSize(); + + if(MitmService.isRunning()) + (Toast.makeText(this, R.string.restart_to_apply, Toast.LENGTH_SHORT)).show(); + } + + // get the file name of the enabled addons + public static Set getEnabledAddons(Context ctx) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + return prefs.getStringSet(ENABLED_ADDONS_PREF, new HashSet<>()); + } + + private void recheckListSize() { + mEmptyText.setVisibility((mAdapter.getCount() == 0) ? View.VISIBLE : View.GONE); + } + + public static List listUserAddons(Context ctx, Uri publicUri) { + List rv = new ArrayList<>(); + + try { + Uri srcFolder = DocumentsContract.buildChildDocumentsUriUsingTree(publicUri, DocumentsContract.getTreeDocumentId(publicUri)); + + try (Cursor cursor = ctx.getContentResolver().query(srcFolder, + new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID }, + null, null, null)) { + if ((cursor != null) && cursor.moveToFirst()) { + do { + Uri uri = DocumentsContract.buildDocumentUriUsingTree(publicUri, cursor.getString(0)); + rv.add(uri); + } while (cursor.moveToNext()); + } + } + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, e.toString()); + } + + return rv; + } + + /* Since we can only access the addons dir via the ContentResolver, we copy all the addons + * to the app private dir to make python import work. */ + public static boolean copyAddonsToPrivDir(Context ctx, String privDir) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + Uri publicUri = Uri.parse(prefs.getString(USER_DIR_PREF, "")); + + if((publicUri.getHost() == null) || !hasUriPersistablePermission(ctx, publicUri)) + return false; + + File privAddons = new File(privDir); + privAddons.delete(); + privAddons.mkdirs(); + + Uri srcFolder = DocumentsContract.buildChildDocumentsUriUsingTree(publicUri, DocumentsContract.getTreeDocumentId(publicUri)); + Log.d(TAG, "Addons source dir: " + srcFolder); + List addonsUris = listUserAddons(ctx, publicUri); + + for(Uri srcUri: addonsUris) { + String path = srcUri.getPath(); + int slash = path.lastIndexOf("/"); + if (slash > 0) { + String srcFname = path.substring(slash + 1); + File outFile = new File(privAddons.getAbsolutePath() + "/" + srcFname); + Log.d(TAG, "Found addon: " + srcFname); + + // Copy from srcUri to outFile + try { + try (InputStream in = new BufferedInputStream(ctx.getContentResolver().openInputStream(srcUri))) { + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(outFile))) { + byte[] bytesIn = new byte[4096]; + int read; + while ((read = in.read(bytesIn)) != -1) + out.write(bytesIn, 0, read); + } + } + } catch (IOException e) { + e.printStackTrace(); + Log.e(TAG, e.toString()); + + return false; + } + } + } + + return true; + } + + private static boolean hasUriPersistablePermission(Context ctx, Uri uri) { + List persistableUris = ctx.getContentResolver().getPersistedUriPermissions(); + + for(UriPermission perm: persistableUris) { + if(perm.getUri().equals(uri) && perm.isReadPermission()) + return true; + } + + return false; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.addons, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if(id == R.id.show_hint) { + Utils.showHintDialog(this, R.string.addons_hint); + return true; + } else if(id == R.id.update) { + refreshAddons(); + return true; + } else if(id == R.id.select_user_dir) { + selectUserDir(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void selectUserDir() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Initial path + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(Environment.getExternalStorageDirectory().getAbsolutePath())); + } + + (Toast.makeText(this, R.string.specify_user_dir, Toast.LENGTH_LONG)).show(); + startActivityForResult(intent, OPEN_DIR_TREE_CODE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if((requestCode == OPEN_DIR_TREE_CODE) && (resultCode == RESULT_OK) && (resultData != null)) { + Uri user_dir = resultData.getData(); + + // Persist access across restarts + getContentResolver().takePersistableUriPermission(user_dir, Intent.FLAG_GRANT_READ_URI_PERMISSION); + + mPrefs.edit() + .putString(USER_DIR_PREF, user_dir.toString()) + .apply(); + + refreshAddons(); + } + } + + @Override + public void onAddonToggled(Addon addon, boolean enabled) { + Set enabledAddons = getEnabledAddons(AddonsActivity.this); + addon.enabled = enabled; + + if(enabled) + enabledAddons.add(addon.fname); + else + enabledAddons.remove(addon.fname); + + mPrefs.edit() + .putStringSet(ENABLED_ADDONS_PREF, enabledAddons) + .apply(); + + refreshAddons(); + } + + @Override + public void onAddonSettingsClicked(Addon addon) { + if(addon.type == Addon.AddonType.JsInjector) { + Intent intent = new Intent(this, JsInjectorActivity.class); + startActivity(intent); + } + } +} diff --git a/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java b/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java new file mode 100644 index 0000000..40952cd --- /dev/null +++ b/app/src/main/java/com/pcapdroid/mitm/AddonsAdapter.java @@ -0,0 +1,92 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PCAPdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PCAPdroid. If not, see . + * + * Copyright 2023 - Emanuele Faranda + */ + +package com.pcapdroid.mitm; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.Switch; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.List; + +public class AddonsAdapter extends ArrayAdapter { + public interface AddonListener { + void onAddonToggled(Addon addon, boolean enabled); + void onAddonSettingsClicked(Addon addon); + } + + private final LayoutInflater mLayoutInflater; + private WeakReference mListener; + + public AddonsAdapter(Context context) { + super(context, R.layout.addon_item); + + mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if(convertView == null) { + convertView = mLayoutInflater.inflate(R.layout.addon_item, parent, false); + + convertView.findViewById(R.id.toggle_btn) + .setOnClickListener((v) -> { + AddonListener listener = mListener.get(); + if(listener != null) + listener.onAddonToggled(getItem(position), ((Switch)v).isChecked()); + }); + + convertView.findViewById(R.id.settings) + .setOnClickListener((v) -> { + AddonListener listener = mListener.get(); + if(listener != null) + listener.onAddonSettingsClicked(getItem(position)); + }); + } + + TextView fname = convertView.findViewById(R.id.fname); + TextView info = convertView.findViewById(R.id.info); + Switch toggle = convertView.findViewById(R.id.toggle_btn); + ImageButton settings = convertView.findViewById(R.id.settings); + + Addon addon = getItem(position); + fname.setText(addon.fname); + info.setText(addon.description); + toggle.setChecked(addon.enabled); + settings.setVisibility((addon.type == Addon.AddonType.UserAddon) ? + View.GONE : View.VISIBLE); + + return convertView; + } + + public void setListener(AddonListener listener) { + mListener = new WeakReference<>(listener); + } + + public void reload(List addons) { + clear(); + addAll(addons); + } +} diff --git a/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java b/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java index 77cf59f..b3b28a3 100644 --- a/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java +++ b/app/src/main/java/com/pcapdroid/mitm/JsInjectorActivity.java @@ -1,8 +1,26 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PCAPdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PCAPdroid. If not, see . + * + * Copyright 2023 - Emanuele Faranda + */ + package com.pcapdroid.mitm; import android.app.Activity; import android.app.AlertDialog; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -10,8 +28,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; @@ -248,7 +264,7 @@ public class JsInjectorActivity extends Activity { int id = item.getItemId(); if(id == R.id.show_hint) { - showHintDialog(R.string.js_injector_hint); + Utils.showHintDialog(this, R.string.js_injector_hint); return true; } else if(id == R.id.add) { showAddScriptDialog(); @@ -260,29 +276,12 @@ public class JsInjectorActivity extends Activity { } refreshScripts(); + return true; } return super.onOptionsItemSelected(item); } - private void showHintDialog(int id) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.hint); - builder.setMessage(getString(id)); - builder.setCancelable(true); - builder.setNeutralButton(android.R.string.ok, - (dialog, id1) -> dialog.cancel()); - - AlertDialog alert = builder.create(); - alert.show(); - - TextView message = (TextView)alert.findViewById(android.R.id.message); - if(message != null) { - message.setMovementMethod(LinkMovementMethod.getInstance()); - message.setText(id); - } - } - private void showAddScriptDialog() { LayoutInflater inflater = LayoutInflater.from(this); View view = inflater.inflate(R.layout.add_script_dialog, null); diff --git a/app/src/main/java/com/pcapdroid/mitm/MainActivity.java b/app/src/main/java/com/pcapdroid/mitm/MainActivity.java index 071965f..340c59f 100644 --- a/app/src/main/java/com/pcapdroid/mitm/MainActivity.java +++ b/app/src/main/java/com/pcapdroid/mitm/MainActivity.java @@ -62,9 +62,9 @@ public class MainActivity extends Activity { (Toast.makeText(this, R.string.app_not_found, Toast.LENGTH_SHORT)).show(); }); - findViewById(R.id.open_js_injector).setOnClickListener(v -> { - Intent intent = new Intent(MainActivity.this, JsInjectorActivity.class); + findViewById(R.id.addons).setOnClickListener(v -> { + Intent intent = new Intent(MainActivity.this, AddonsActivity.class); startActivity(intent); }); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/pcapdroid/mitm/MitmService.java b/app/src/main/java/com/pcapdroid/mitm/MitmService.java index d6e9da6..514252f 100644 --- a/app/src/main/java/com/pcapdroid/mitm/MitmService.java +++ b/app/src/main/java/com/pcapdroid/mitm/MitmService.java @@ -36,6 +36,7 @@ import com.chaquo.python.Python; import java.io.IOException; import java.lang.ref.WeakReference; +import java.util.Set; import com.pcapdroid.mitm.MitmAPI.MitmConfig; @@ -47,12 +48,18 @@ public class MitmService extends Service implements Runnable { Thread mThread; PyObject mitm; MitmConfig mConf; + String m_home; @Override public void onCreate() { Python py = Python.getInstance(); mitm = py.getModule("mitm"); + PyObject os = py.getModule("os"); + PyObject env = os.get("environ"); + m_home = env.callAttr("get", "HOME").toString(); + Log.d(TAG, "Chaquopy home at " + m_home); + INSTANCE = this; super.onCreate(); } @@ -157,8 +164,12 @@ public class MitmService extends Service implements Runnable { // Transparent mode is used with root mode where we capture the internet interface, so we must dump the server connection boolean dump_client = !mConf.transparentMode; + AddonsActivity.copyAddonsToPrivDir(this, m_home); + String[] enabled_addons = AddonsActivity.getEnabledAddons(this).toArray(new String[]{}); + try { - mitm.callAttr("run", mFd.getFd(), dump_client, mConf.dumpMasterSecrets, mConf.shortPayload, args); + mitm.callAttr("run", mFd.getFd(), enabled_addons, dump_client, + mConf.dumpMasterSecrets, mConf.shortPayload, args); } finally { try { if(mFd != null) @@ -244,4 +255,8 @@ public class MitmService extends Service implements Runnable { if(instance != null) instance.mitm.callAttr("reloadJsUserscripts"); } + + public static boolean isRunning() { + return (INSTANCE != null); + } } diff --git a/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java b/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java index 6ef75e2..d4f6990 100644 --- a/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java +++ b/app/src/main/java/com/pcapdroid/mitm/ScriptsAdapter.java @@ -1,3 +1,22 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PCAPdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PCAPdroid. If not, see . + * + * Copyright 2023 - Emanuele Faranda + */ + package com.pcapdroid.mitm; import android.content.Context; diff --git a/app/src/main/java/com/pcapdroid/mitm/Utils.java b/app/src/main/java/com/pcapdroid/mitm/Utils.java new file mode 100644 index 0000000..5d56616 --- /dev/null +++ b/app/src/main/java/com/pcapdroid/mitm/Utils.java @@ -0,0 +1,45 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PCAPdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PCAPdroid. If not, see . + * + * Copyright 2023 - Emanuele Faranda + */ + +package com.pcapdroid.mitm; + +import android.app.AlertDialog; +import android.content.Context; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + +public class Utils { + public static void showHintDialog(Context ctx, int id) { + AlertDialog.Builder builder = new AlertDialog.Builder(ctx); + builder.setTitle(R.string.hint); + builder.setMessage(ctx.getString(id)); + builder.setCancelable(true); + builder.setNeutralButton(android.R.string.ok, + (dialog, id1) -> dialog.cancel()); + + AlertDialog alert = builder.create(); + alert.show(); + + TextView message = (TextView)alert.findViewById(android.R.id.message); + if(message != null) { + message.setMovementMethod(LinkMovementMethod.getInstance()); + message.setText(id); + } + } +} diff --git a/app/src/main/python/mitm.py b/app/src/main/python/mitm.py index b2916e2..7b4b14b 100644 --- a/app/src/main/python/mitm.py +++ b/app/src/main/python/mitm.py @@ -21,6 +21,7 @@ import os MITMPROXY_CONF_DIR = os.environ["HOME"] + "/.mitmproxy" +USER_ADDONS_DIR = os.environ["HOME"] + "/mitmproxy-addons" CA_CERT_PATH = MITMPROXY_CONF_DIR + "/mitmproxy-ca-cert.cer" from mitmproxy import options @@ -37,6 +38,7 @@ import traceback import socket import asyncio import sys +import importlib master = None pcapdroid = None @@ -83,9 +85,26 @@ def server_event_proxy(handler, event): pcapdroid.server_error(hook_data) return orig_server_event(handler, event) +def load_addon(modname, addons): + try: + m = importlib.import_module(modname) + if m and hasattr(m, "addons") and isinstance(m.addons, list): + for addon in m.addons: + addons.add(addon) + except Exception: + sys.stderr.write("Failed to load addon " + modname) + sys.stderr.write(traceback.format_exc()) + +def jarray_to_set(arr): + rv = set() + for elem in arr: + rv.add(elem) + return rv + # Entrypoint: runs mitmproxy # From mitmproxy.tools.main.run, without the signal handlers -def run(fd: int, dump_client: bool, dump_keylog: bool, short_payload: bool, mitm_args: str): +def run(fd: int, jenabled_addons, dump_client: bool, dump_keylog: bool, + short_payload: bool, mitm_args: str): global master global running global pcapdroid, js_injector @@ -99,14 +118,28 @@ def run(fd: int, dump_client: bool, dump_keylog: bool, short_payload: bool, mitm opts = options.Options() master = dump.DumpMaster(opts) - # JsInjector addon (before PCAPdroid) - js_injector = JsInjector() - master.addons.add(js_injector) - # instantiate PCAPdroid early to send error log via the API pcapdroid = PCAPdroid(sock, AddonOpts(dump_client, dump_keylog, short_payload)) + + enabled_addons = jarray_to_set(jenabled_addons) + + # Load addons (order is important) master.addons.add(pcapdroid) + # JsInjector addon + if "Js Injector" in enabled_addons: + js_injector = JsInjector() + master.addons.add(js_injector) + + sys.path.append(USER_ADDONS_DIR) + for f in os.listdir(USER_ADDONS_DIR): + if f.endswith(".py"): + fname = f[:-3] + + if fname in enabled_addons: + print("Loading addon: " + f) + load_addon(fname, master.addons) + print("mitmdump " + mitm_args) parser = cmdline.mitmdump(opts) args = parser.parse_args(mitm_args.split()) diff --git a/app/src/main/res/drawable/ic_baseline_settings.xml b/app/src/main/res/drawable/ic_baseline_settings.xml new file mode 100644 index 0000000..6db7490 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/addon_item.xml b/app/src/main/res/layout/addon_item.xml new file mode 100644 index 0000000..c136927 --- /dev/null +++ b/app/src/main/res/layout/addon_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index ec3a2ef..76bfff1 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -140,10 +140,10 @@ android:layout_gravity="center" />