Rework app settings in status view quick settings

This commit is contained in:
emanuele-f 2021-03-02 17:39:39 +01:00
parent 94b93f3175
commit 6f009ce19a
18 changed files with 460 additions and 230 deletions

View File

@ -137,8 +137,8 @@ public class CaptureService extends VpnService implements Runnable {
public_dns = Utils.getDnsServer(app_ctx);
vpn_dns = VPN_VIRTUAL_DNS_SERVER;
vpn_ipv4 = VPN_IP_ADDRESS;
app_filter = settings.getString(Prefs.PREF_APP_FILTER);
app_filter = Prefs.getAppFilter(prefs);
collector_address = Prefs.getCollectorIp(prefs);
collector_port = Prefs.getCollectorPort(prefs);
http_server_port = Prefs.getHttpServerPort(prefs);
@ -197,7 +197,7 @@ public class CaptureService extends VpnService implements Runnable {
.addRoute("128.0.0.0", 1)
.addDnsServer(vpn_dns);
if(app_filter != null) {
if((app_filter != null) && (!app_filter.isEmpty())) {
Log.d(TAG, "Setting app filter: " + app_filter);
try {

View File

@ -20,8 +20,15 @@
package com.emanuelef.remote_capture;
import android.app.Activity;
import android.app.Dialog;
import android.app.Service;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ScaleDrawable;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
@ -30,6 +37,7 @@ import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
@ -249,7 +257,7 @@ public class Utils {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
public static void showAppSelectionDialog(Activity activity, List<AppDescriptor> appsData, AppsListView.OnSelectedAppListener listener) {
public static Dialog getAppSelectionDialog(Activity activity, List<AppDescriptor> appsData, AppsListView.OnSelectedAppListener listener) {
View dialogLayout = activity.getLayoutInflater().inflate(R.layout.apps_selector, null);
SearchView searchView = dialogLayout.findViewById(R.id.apps_search);
AppsListView apps = dialogLayout.findViewById(R.id.apps_list);
@ -264,15 +272,16 @@ public class Utils {
builder.setView(dialogLayout);
final AlertDialog alert = builder.create();
alert.setCanceledOnTouchOutside(true);
apps.setSelectedAppListener(app -> {
listener.onSelectedApp(app);
// dismiss the dialog
alert.cancel();
alert.dismiss();
});
alert.show();
return alert;
}
public static String getUniquePcapFileName(Context context) {
@ -280,4 +289,14 @@ public class Utils {
final DateFormat fmt = new SimpleDateFormat("dd_MMM_HH_mm_ss", locale);
return "PCAPdroid_" + fmt.format(new Date()) + ".pcap";
}
public static Drawable scaleDrawable(Resources res, Drawable drawable, int new_x, int new_y) {
Bitmap bitmap = Bitmap.createBitmap(new_x, new_y, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return new BitmapDrawable(res, bitmap);
}
}

View File

@ -27,7 +27,6 @@ import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.VpnService;
@ -52,10 +51,7 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.emanuelef.remote_capture.AppsLoader;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
import com.emanuelef.remote_capture.interfaces.AppsLoadListener;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.AppState;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.model.Prefs;
@ -64,29 +60,19 @@ import com.emanuelef.remote_capture.Utils;
import com.google.android.material.navigation.NavigationView;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import cat.ereza.customactivityoncrash.config.CaocConfig;
public class MainActivity extends AppCompatActivity implements AppsLoadListener, NavigationView.OnNavigationItemSelectedListener {
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private SharedPreferences mPrefs;
private Menu mMenu;
private MenuItem mMenuItemStartBtn;
private MenuItem mMenuItemAppSel;
private MenuItem mMenuSettings;
private Drawable mFilterIcon;
private String mFilterApp;
private boolean mOpenAppsWhenDone;
private List<AppDescriptor> mInstalledApps;
private AppState mState;
private AppStateListener mListener;
private AppDescriptor mNoFilterApp;
private Uri mPcapUri;
private BroadcastReceiver mReceiver;
private String mPcapFname;
private static final String TAG = "Main";
@ -101,19 +87,8 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Drawable icon = ContextCompat.getDrawable(this, android.R.color.transparent);
mNoFilterApp = new AppDescriptor("", icon, this.getResources().getString(R.string.no_filter), -1, false, true);
mFilterApp = CaptureService.getAppFilter();
mPcapUri = CaptureService.getPcapUri();
if((mFilterApp == null) && (savedInstanceState != null)) {
// Possibly get the temporary filter
mFilterApp = savedInstanceState.getString("FilterApp");
}
mOpenAppsWhenDone = false;
CaocConfig.Builder.create()
.errorDrawable(R.drawable.ic_app_crash)
.apply();
@ -122,10 +97,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
(new AppsLoader(this))
.setAppsLoadListener(this)
.loadAllApps();
/* Register for service status */
mReceiver = new BroadcastReceiver() {
@Override
@ -143,6 +114,7 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE)) {
showPcapActionDialog(mPcapUri);
mPcapUri = null;
mPcapFname = null;
}
appStateReady();
@ -200,13 +172,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putString("FilterApp", mFilterApp);
}
@Override
public void onResume() {
super.onResume();
@ -215,38 +180,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
initAppState();
}
@Override
public void onAppsInfoLoaded(Map<Integer, AppDescriptor> apps) {
// TODO optimize: show the apps even when icon not loaded
}
@Override
public void onAppsIconsLoaded(Map<Integer, AppDescriptor> apps) {
mInstalledApps = new ArrayList<>();
AppDescriptor filterApp = null;
for (Map.Entry<Integer, AppDescriptor> pair : apps.entrySet()) {
AppDescriptor app = pair.getValue();
if(!app.isVirtual()) {
mInstalledApps.add(app);
if (app.getPackageName().equals(mFilterApp))
filterApp = app;
}
}
Collections.sort(mInstalledApps);
if (filterApp != null) {
/* An filter is active, try to set the corresponding app image */
setSelectedApp(filterApp);
}
if (mOpenAppsWhenDone)
openAppSelector();
}
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
@ -300,7 +233,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
ContextCompat.getDrawable(this, android.R.drawable.ic_media_play));
mMenuItemStartBtn.setTitle(R.string.start_button);
mMenuItemStartBtn.setEnabled(true);
mMenuItemAppSel.setEnabled(true);
mMenuSettings.setEnabled(true);
}
@ -310,7 +242,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
mMenuItemStartBtn.setEnabled(false);
mMenuSettings.setEnabled(false);
mMenuItemAppSel.setEnabled(false);
}
public void appStateRunning() {
@ -322,7 +253,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
mMenuItemStartBtn.setTitle(R.string.stop_button);
mMenuItemStartBtn.setEnabled(true);
mMenuSettings.setEnabled(false);
mMenuItemAppSel.setEnabled(false);
}
public void appStateStopping() {
@ -339,11 +269,7 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
mMenu = menu;
mMenuItemStartBtn = mMenu.findItem(R.id.action_start);
mMenuItemAppSel = mMenu.findItem(R.id.action_show_app_filter);
mMenuSettings = mMenu.findItem(R.id.action_settings);
mFilterIcon = mMenuItemAppSel.getIcon();
initAppState();
return true;
@ -386,13 +312,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
startActivity(intent);
return true;
} else if (id == R.id.action_show_app_filter) {
if(mFilterApp != null)
setSelectedApp(null);
else
openAppSelector();
return true;
}
return super.onOptionsItemSelected(item);
@ -410,7 +329,6 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
if((mPcapUri != null) && (Prefs.getDumpMode(mPrefs) == Prefs.DumpMode.PCAP_FILE))
bundle.putString(Prefs.PREF_PCAP_URI, mPcapUri.toString());
bundle.putString(Prefs.PREF_APP_FILTER, mFilterApp);
intent.putExtra("settings", bundle);
Log.d(TAG, "onActivityResult -> start CaptureService");
@ -426,6 +344,7 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
} else if(requestCode == REQUEST_CODE_PCAP_FILE) {
if(resultCode == RESULT_OK) {
mPcapUri = data.getData();
mPcapFname = null;
Log.d(TAG, "PCAP to write: " + mPcapUri.toString());
toggleService();
@ -496,11 +415,11 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
if((cursor == null) || !cursor.moveToFirst())
return;
// If file is empty, delete it
long file_size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
String fname = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor.close();
// If file is empty, delete it
if(file_size == 0) {
Log.d(TAG, "PCAP file is empty, deleting");
@ -550,40 +469,29 @@ public class MainActivity extends AppCompatActivity implements AppsLoadListener,
return(mState);
}
public void setSelectedApp(AppDescriptor app) {
if(app == null)
app = mNoFilterApp;
public String getPcapFname() {
if((mState == AppState.running) && (mPcapUri != null)) {
if(mPcapFname != null)
return mPcapFname;
Log.d(TAG, "Selected app: " + app.getUid());
Cursor cursor;
if(app.getUid() != -1) {
// an app has been selected
mFilterApp = app.getPackageName();
// clone the drawable to avoid a "zoom-in" effect when clicked
Drawable drawable = (app.getIcon() != null) ? Objects.requireNonNull(app.getIcon().getConstantState()).newDrawable() : null;
if(drawable != null) {
mMenuItemAppSel.setIcon(drawable);
mMenuItemAppSel.setTitle(R.string.remove_app_filter);
try {
cursor = getContentResolver().query(mPcapUri, null, null, null, null);
} catch (Exception e) {
return null;
}
} else {
// no filter
mFilterApp = null;
mMenuItemAppSel.setIcon(mFilterIcon);
mMenuItemAppSel.setTitle(R.string.set_app_filter);
}
}
private void openAppSelector() {
if(mInstalledApps == null) {
/* The applications loader has not finished yet. */
mOpenAppsWhenDone = true;
Utils.showToast(this, R.string.apps_loading_please_wait);
return;
if((cursor == null) || !cursor.moveToFirst())
return null;
String fname = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
cursor.close();
mPcapFname = fname;
return fname;
}
mOpenAppsWhenDone = false;
Utils.showAppSelectionDialog(this, mInstalledApps, this::setSelectedApp);
return null;
}
}

View File

@ -51,10 +51,6 @@ public class SettingsActivity extends AppCompatActivity {
}
public static class SettingsFragment extends PreferenceFragmentCompat {
private EditTextPreference mRemoteCollectorIp;
private EditTextPreference mRemoteCollectorPort;
private EditTextPreference mHttpServerPort;
private ListPreference mDumpModePref;
private SwitchPreference mTlsDecryptionEnabled;
private EditTextPreference mTlsProxyIp;
private EditTextPreference mTlsProxyPort;
@ -63,18 +59,11 @@ public class SettingsActivity extends AppCompatActivity {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
mDumpModePref = findPreference(Prefs.PREF_PCAP_DUMP_MODE);
mDumpModePref.setOnPreferenceChangeListener((preference, newValue) -> {
dumpPrefsHideShow((String) newValue);
return true;
});
setupUdpExporterPrefs();
setupHttpServerPrefs();
setupTlsProxyPrefs();
tlsDecryptionHideShow(mTlsDecryptionEnabled.isChecked());
dumpPrefsHideShow(mDumpModePref.getValue());
}
private boolean validatePort(String value) {
@ -88,21 +77,21 @@ public class SettingsActivity extends AppCompatActivity {
private void setupUdpExporterPrefs() {
/* Collector IP validation */
mRemoteCollectorIp = findPreference(Prefs.PREF_COLLECTOR_IP_KEY);
EditTextPreference mRemoteCollectorIp = findPreference(Prefs.PREF_COLLECTOR_IP_KEY);
mRemoteCollectorIp.setOnPreferenceChangeListener((preference, newValue) -> {
Matcher matcher = Patterns.IP_ADDRESS.matcher(newValue.toString());
return(matcher.matches());
});
/* Collector port validation */
mRemoteCollectorPort = findPreference(Prefs.PREF_COLLECTOR_PORT_KEY);
EditTextPreference mRemoteCollectorPort = findPreference(Prefs.PREF_COLLECTOR_PORT_KEY);
mRemoteCollectorPort.setOnBindEditTextListener(editText -> editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED));
mRemoteCollectorPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
}
private void setupHttpServerPrefs() {
/* HTTP Server port validation */
mHttpServerPort = findPreference(Prefs.PREF_HTTP_SERVER_PORT);
EditTextPreference mHttpServerPort = findPreference(Prefs.PREF_HTTP_SERVER_PORT);
mHttpServerPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
}
@ -126,41 +115,6 @@ public class SettingsActivity extends AppCompatActivity {
mTlsProxyPort.setOnPreferenceChangeListener((preference, newValue) -> validatePort(newValue.toString()));
}
/* This implements a radio-button like behaviour */
private void dumpPrefsHideShow(String dumpMode) {
Prefs.DumpMode mode = Prefs.getDumpMode(dumpMode);
boolean show_http_prefs = (mode == Prefs.DumpMode.HTTP_SERVER);
boolean show_udp_prefs = (mode == Prefs.DumpMode.UDP_EXPORTER);
/* HTTP Server */
mHttpServerPort.setVisible(show_http_prefs);
/* UDP Receiver */
mRemoteCollectorIp.setVisible(show_udp_prefs);
mRemoteCollectorPort.setVisible(show_udp_prefs);
/* Adjust label */
int summary_id;
switch(mode) {
case HTTP_SERVER:
summary_id = R.string.http_server_info;
break;
case PCAP_FILE:
summary_id = R.string.pcap_file_info;
break;
case UDP_EXPORTER:
summary_id = R.string.udp_exporter_info;
break;
case NONE:
default:
summary_id = R.string.no_dump_info;
break;
}
mDumpModePref.setSummary(summary_id);
}
private void tlsDecryptionHideShow(boolean decryptionEnabled) {
mTlsProxyIp.setVisible(decryptionEnabled);
mTlsProxyPort.setVisible(decryptionEnabled);

View File

@ -76,8 +76,10 @@ public class AppsAdapter extends RecyclerView.Adapter<AppsAdapter.AppViewHolder>
AppDescriptor app = getItem(position);
holder.textInListView.setText(app.getName());
holder.imageInListView.setImageDrawable(app.getIcon());
holder.packageInListView.setText(app.getPackageName());
if(app.getIcon() != null)
holder.imageInListView.setImageDrawable(app.getIcon());
}
@Override

View File

@ -0,0 +1,84 @@
package com.emanuelef.remote_capture.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.emanuelef.remote_capture.R;
public class DumpModesAdapter extends BaseAdapter {
private final Context mContext;
private final LayoutInflater mInflater;
private final DumpModeInfo[] mModes;
public class DumpModeInfo {
public final String key;
public final String label;
public final String description;
public DumpModeInfo(String _key, String _label, String _descr) {
key = _key;
label = _label;
description = _descr;
}
}
public DumpModesAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
String[] keys = mContext.getResources().getStringArray(R.array.pcap_dump_modes);
String[] labels = mContext.getResources().getStringArray(R.array.pcap_dump_modes_labels);
String[] descriptions = mContext.getResources().getStringArray(R.array.pcap_dump_modes_descriptions);
assert ((keys.length == labels.length) && (keys.length == descriptions.length));
mModes = new DumpModeInfo[keys.length];
for(int i=0; i<keys.length; i++) {
mModes[i] = new DumpModeInfo(keys[i], labels[i], descriptions[i]);
}
}
public int getModePos(String key) {
for(int i=0; i<mModes.length; i++) {
if(key.equals(mModes[i].key))
return i;
}
return 0;
}
@Override
public int getCount() {
return mModes.length;
}
@Override
public Object getItem(int position) {
return mModes[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null)
convertView = mInflater.inflate(R.layout.quick_settings_item, parent, false);
DumpModeInfo mode = (DumpModeInfo) getItem(position);
TextView title =convertView.findViewById(R.id.title);
TextView description =convertView.findViewById(R.id.description);
title.setText(mode.label);
description.setText(mode.description);
return convertView;
}
}

View File

@ -413,7 +413,7 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
Collections.sort(appsData);
Utils.showAppSelectionDialog(getActivity(), appsData, app -> setUidFilter(app.getUid()));
Utils.getAppSelectionDialog(getActivity(), appsData, app -> setUidFilter(app.getUid())).show();
}
private void setUidFilter(int uid) {

View File

@ -20,6 +20,7 @@
package com.emanuelef.remote_capture.fragments;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -33,33 +34,54 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.AppsLoader;
import com.emanuelef.remote_capture.activities.InspectorActivity;
import com.emanuelef.remote_capture.adapters.DumpModesAdapter;
import com.emanuelef.remote_capture.interfaces.AppsLoadListener;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.AppState;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.Utils;
import com.emanuelef.remote_capture.activities.MainActivity;
import com.emanuelef.remote_capture.activities.StatsActivity;
import com.emanuelef.remote_capture.interfaces.AppStateListener;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.model.VPNStats;
import com.emanuelef.remote_capture.views.AppsListView;
public class StatusFragment extends Fragment implements AppStateListener {
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class StatusFragment extends Fragment implements AppStateListener, AppsLoadListener {
private static final String TAG = "StatusFragment";
private TextView mCollectorInfo;
private TextView mCaptureStatus;
private TextView mInspectorLink;
private View mQuickSettings;
private MainActivity mActivity;
private SharedPreferences mPrefs;
private BroadcastReceiver mReceiver;
private TextView mFilterDescription;
private SwitchCompat mAppFilterSwitch;
private String mAppFilter;
private boolean mOpenAppsWhenDone;
private List<AppDescriptor> mInstalledApps;
AppsListView mOpenAppsList;
@Override
public void onAttach(@NonNull Context context) {
@ -87,7 +109,47 @@ public class StatusFragment extends Fragment implements AppStateListener {
mCollectorInfo = view.findViewById(R.id.collector_info);
mCaptureStatus = view.findViewById(R.id.status_view);
mInspectorLink = view.findViewById(R.id.inspector_link);
mQuickSettings = view.findViewById(R.id.quick_settings);
mPrefs = PreferenceManager.getDefaultSharedPreferences(mActivity);
mAppFilter = Prefs.getAppFilter(mPrefs);
mOpenAppsWhenDone = false;
DumpModesAdapter dumpModeAdapter = new DumpModesAdapter(getContext());
Spinner dumpMode = view.findViewById(R.id.dump_mode_spinner);
dumpMode.setAdapter(dumpModeAdapter);
int curSel = dumpModeAdapter.getModePos(mPrefs.getString(Prefs.PREF_PCAP_DUMP_MODE, Prefs.DEFAULT_DUMP_MODE));
dumpMode.setSelection(curSel);
dumpMode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
DumpModesAdapter.DumpModeInfo mode = (DumpModesAdapter.DumpModeInfo) dumpModeAdapter.getItem(position);
SharedPreferences.Editor editor = mPrefs.edit();
editor.putString(Prefs.PREF_PCAP_DUMP_MODE, mode.key);
editor.apply();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
mAppFilterSwitch = view.findViewById(R.id.app_filter_switch);
View filterRow = view.findViewById(R.id.app_filter_text);
TextView filterTitle = filterRow.findViewById(R.id.title);
mFilterDescription = filterRow.findViewById(R.id.description);
filterTitle.setText(R.string.app_filter);
mAppFilterSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if(isChecked) {
if((mAppFilter == null) || (mAppFilter.isEmpty()))
openAppFilterSelector();
} else
setAppFilter(null);
});
refreshFilterInfo();
mCaptureStatus.setOnClickListener(v -> {
if(mActivity.getState() == AppState.running) {
@ -101,11 +163,6 @@ public class StatusFragment extends Fragment implements AppStateListener {
setupInspectorLinK();
mPrefs.registerOnSharedPreferenceChangeListener((sharedPreferences, s) -> {
if((mActivity != null) && (mActivity.getState() == AppState.ready))
refreshPcapDumpInfo();
});
/* Register for stats update */
mReceiver = new BroadcastReceiver() {
@Override
@ -119,6 +176,10 @@ public class StatusFragment extends Fragment implements AppStateListener {
/* Important: call this after all the fields have been initialized */
mActivity.setAppStateListener(this);
(new AppsLoader(mActivity))
.setAppsLoadListener(this)
.loadAllApps();
}
@Override
@ -131,6 +192,59 @@ public class StatusFragment extends Fragment implements AppStateListener {
}
}
private AppDescriptor findAppByPackage(String pkgname) {
if(mInstalledApps == null)
return null;
for(AppDescriptor app : mInstalledApps) {
if(pkgname.equals(app.getPackageName()))
return app;
}
return null;
}
private void refreshFilterInfo() {
if((mAppFilter == null) || (mAppFilter.isEmpty())) {
mFilterDescription.setText(R.string.no_app_filter);
mFilterDescription.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
mAppFilterSwitch.setChecked(false);
return;
}
mAppFilterSwitch.setChecked(true);
AppDescriptor app = findAppByPackage(mAppFilter);
String description;
if(app == null)
description = mAppFilter;
else {
description = app.getName() + " (" + app.getPackageName() + ")";
if(app.getIcon() != null) {
int height = mFilterDescription.getMeasuredHeight();
Drawable drawable = Utils.scaleDrawable(getResources(), app.getIcon(), height, height);
if(drawable != null)
mFilterDescription.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
else
mFilterDescription.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
}
mFilterDescription.setText(description);
}
private void setAppFilter(AppDescriptor filter) {
SharedPreferences.Editor editor = mPrefs.edit();
mAppFilter = (filter != null) ? filter.getPackageName() : "";
editor.putString(Prefs.PREF_APP_FILTER, mAppFilter);
editor.apply();
refreshFilterInfo();
}
private void setupInspectorLinK() {
int color = ContextCompat.getColor(mActivity, android.R.color.tab_indicator_text);
@ -153,41 +267,53 @@ public class StatusFragment extends Fragment implements AppStateListener {
mCaptureStatus.setText(Utils.formatBytes(stats.bytes_sent + stats.bytes_rcvd));
}
private void refreshPcapDumpInfo() {
String info;
String modeName;
private void refreshPcapDumpInfo() {
String info = "";
Prefs.DumpMode mode = CaptureService.isServiceActive() ? CaptureService.getDumpMode() : Prefs.getDumpMode(mPrefs);
Prefs.DumpMode mode = CaptureService.getDumpMode();
switch (mode) {
case NONE:
info = getString(R.string.no_dump_info);
break;
case HTTP_SERVER:
modeName = getResources().getString(R.string.http_server);
info = String.format(getResources().getString(R.string.http_server_status),
Utils.getLocalIPAddress(mActivity), CaptureService.getHTTPServerPort());
break;
case PCAP_FILE:
modeName = getResources().getString(R.string.pcap_file);
info = "";
info = getString(R.string.pcap_file_info);
if(mActivity != null) {
String fname = mActivity.getPcapFname();
if(fname != null)
info = fname;
}
break;
case UDP_EXPORTER:
modeName = getResources().getString(R.string.udp_exporter);
info = String.format(getResources().getString(R.string.collector_info),
CaptureService.getCollectorAddress(), CaptureService.getCollectorPort());
break;
default:
modeName = getResources().getString(R.string.no_dump);
info = "";
break;
}
if(!CaptureService.isServiceActive()) {
info = getResources().getString(R.string.dump_mode) + ": " + modeName;
if(Prefs.getTlsDecryptionEnabled(mPrefs))
info += " (" + getResources().getString(R.string.with_tls_decryption) + ")";
}
mCollectorInfo.setText(info);
// Check if a filter is set
if(mAppFilter != null) {
AppDescriptor app = findAppByPackage(mAppFilter);
if((app != null) && (app.getIcon() != null)) {
int height = mCollectorInfo.getMeasuredHeight();
Drawable drawable = Utils.scaleDrawable(getResources(), app.getIcon(), height, height);
if(drawable != null)
mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);
else
mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
} else
mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
} else
mCollectorInfo.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
@Override
@ -196,15 +322,75 @@ public class StatusFragment extends Fragment implements AppStateListener {
case ready:
mCaptureStatus.setText(R.string.ready);
mInspectorLink.setVisibility(View.GONE);
refreshPcapDumpInfo();
mCollectorInfo.setVisibility(View.GONE);
mQuickSettings.setVisibility(View.VISIBLE);
break;
case running:
mCaptureStatus.setText(Utils.formatBytes(CaptureService.getBytes()));
mInspectorLink.setVisibility(View.VISIBLE);
mCollectorInfo.setVisibility(View.VISIBLE);
mQuickSettings.setVisibility(View.GONE);
refreshPcapDumpInfo();
break;
default:
break;
}
}
private void loadInstalledApps(Map<Integer, AppDescriptor> apps, boolean with_icons) {
mInstalledApps = new ArrayList<>();
for(Map.Entry<Integer, AppDescriptor> pair : apps.entrySet()) {
AppDescriptor app = pair.getValue();
if(!app.isVirtual())
mInstalledApps.add(app);
}
Collections.sort(mInstalledApps);
refreshFilterInfo();
if(mOpenAppsWhenDone && mAppFilterSwitch.isChecked())
openAppFilterSelector();
}
private void openAppFilterSelector() {
if(mInstalledApps == null) {
// Applications not loaded yet
mOpenAppsWhenDone = true;
Utils.showToast(getContext(), R.string.apps_loading_please_wait);
return;
}
mOpenAppsWhenDone = false;
Dialog dialog = Utils.getAppSelectionDialog(mActivity, mInstalledApps, this::setAppFilter);
dialog.setOnCancelListener(dialog1 -> {
setAppFilter(null);
});
dialog.setOnDismissListener(dialog1 -> {
mOpenAppsList = null;
});
dialog.show();
// NOTE: run this after dialog.show
mOpenAppsList = (AppsListView) dialog.findViewById(R.id.apps_list);
}
@Override
public void onAppsInfoLoaded(Map<Integer, AppDescriptor> apps) {
loadInstalledApps(apps, false);
}
@Override
public void onAppsIconsLoaded(Map<Integer, AppDescriptor> apps) {
loadInstalledApps(apps, true);
// Possibly update the icons
if(mOpenAppsList != null) {
Log.d(TAG, "reloading app icons in dialog");
mOpenAppsList.setApps(mInstalledApps);
}
}
}

View File

@ -34,6 +34,7 @@ public class Prefs {
public static final String PREF_HTTP_SERVER_PORT = "http_server_port";
public static final String PREF_PCAP_DUMP_MODE = "pcap_dump_mode";
public static final String PREF_PCAP_URI = "pcap_path";
public static final String DEFAULT_DUMP_MODE = DUMP_HTTP_SERVER;
public enum DumpMode {
NONE,
@ -56,9 +57,10 @@ public class Prefs {
/* Prefs with defaults */
public static String getCollectorIp(SharedPreferences p) { return(p.getString(PREF_COLLECTOR_IP_KEY, "127.0.0.1")); }
public static int getCollectorPort(SharedPreferences p) { return(Integer.parseInt(p.getString(PREF_COLLECTOR_PORT_KEY, "1234"))); }
public static DumpMode getDumpMode(SharedPreferences p) { return(getDumpMode(p.getString(PREF_PCAP_DUMP_MODE, DUMP_HTTP_SERVER))); }
public static DumpMode getDumpMode(SharedPreferences p) { return(getDumpMode(p.getString(PREF_PCAP_DUMP_MODE, DEFAULT_DUMP_MODE))); }
public static int getHttpServerPort(SharedPreferences p) { return(Integer.parseInt(p.getString(Prefs.PREF_HTTP_SERVER_PORT, "8080"))); }
public static boolean getTlsDecryptionEnabled(SharedPreferences p) { return(p.getBoolean(PREF_TLS_DECRYPTION_ENABLED_KEY, false)); }
public static String getTlsProxyAddress(SharedPreferences p) { return(p.getString(PREF_TLS_PROXY_IP_KEY, "0.0.0.0")); }
public static int getTlsProxyPort(SharedPreferences p) { return(Integer.parseInt(p.getString(Prefs.PREF_TLS_PROXY_PORT_KEY, "8080"))); }
public static String getAppFilter(SharedPreferences p) { return(p.getString(PREF_APP_FILTER, "")); }
}

View File

@ -113,8 +113,12 @@ public class AppsListView extends EmptyRecyclerView implements SearchView.OnQuer
public void setApps(List<AppDescriptor> installedApps) {
mInstalledApps = installedApps;
mAdapter = new AppsAdapter(getContext(), mInstalledApps);
setAdapter(mAdapter);
if(mAdapter == null) {
mAdapter = new AppsAdapter(getContext(), mInstalledApps);
setAdapter(mAdapter);
} else
mAdapter.notifyDataSetChanged();
}
public void setSelectedAppListener(final OnSelectedAppListener listener) {

View File

@ -23,12 +23,17 @@
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/status_fragment"
android:name="com.emanuelef.remote_capture.fragments.StatusFragment"
<ScrollView
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/status"/>
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/status_fragment"
android:name="com.emanuelef.remote_capture.fragments.StatusFragment"
tools:layout="@layout/status"/>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:padding="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:theme="@style/QuickSettings.Title"
tools:text="Title"
android:layout_width="wrap_content"
android:layout_marginBottom="2dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/description"
tools:text="Description"
android:drawablePadding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -14,7 +14,8 @@
android:layout_marginTop="60dp"
android:gravity="center"
android:autoLink="web"
android:drawablePadding="5dp"
android:drawablePadding="10dp"
tools:text="Collector Info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
@ -24,11 +25,12 @@
android:id="@+id/inspector_link"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="@string/inspector"
android:drawablePadding="5dp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
@ -36,8 +38,8 @@
<TextView
android:id="@+id/status_view"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_width="220dp"
android:layout_height="220dp"
android:layout_marginTop="50dp"
android:background="@drawable/rounded_bg"
android:gravity="center"
@ -48,4 +50,42 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/quick_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="32dp"
android:padding="10dp"
app:layout_constraintTop_toBottomOf="@id/status_view"
app:layout_constraintStart_toStartOf="parent">
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/dump_mode_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
android:prompt="@string/dump_mode"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:paddingEnd="0dp">
<include layout="@layout/quick_settings_item"
android:id="@+id/app_filter_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="50dp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/app_filter_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,13 +9,6 @@
android:icon="@android:drawable/ic_media_play"
app:showAsAction="always" />
<item
android:id="@+id/action_show_app_filter"
android:title="@string/set_app_filter"
android:orderInCategory="50"
android:icon="@drawable/ic_filter_alt"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_settings"
android:title="@string/title_activity_settings"

View File

@ -13,4 +13,11 @@
<item>@string/pcap_file</item>
<item>@string/udp_exporter</item>
</string-array>
<string-array name="pcap_dump_modes_descriptions">
<item>@string/no_dump_info</item>
<item>@string/http_server_info</item>
<item>@string/pcap_file_info</item>
<item>@string/udp_exporter_info</item>
</string-array>
</resources>

View File

@ -47,8 +47,8 @@
<string name="http_server_info">Start an HTTP server for the PCAP download</string>
<string name="udp_exporter_info">Sends the PCAP to a remote UDP receiver</string>
<string name="http_server_port">HTTP Server Port</string>
<string name="receiver_ip_address">Receiver IP Address</string>
<string name="receiver_port">Receiver Port</string>
<string name="receiver_ip_address">Collector IP Address</string>
<string name="receiver_port">Collector Port</string>
<string name="dump_mode">Dump Mode</string>
<string name="tls_proxy_ip_address">mitmproxy IP Address</string>
<string name="tls_proxy_port">mitmproxy Port</string>
@ -99,5 +99,6 @@
<string name="delete_error">Could not delete file</string>
<string name="capture_running">Capture running</string>
<string name="notification_msg">%1$s captured, %2$s connections</string>
<string name="no_app_filter">No app filter set</string>
</resources>

View File

@ -17,4 +17,10 @@
<style name="PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="QuickSettings.Title" parent="AppTheme">
<item name="android:textSize">18sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@android:color/tab_indicator_text</item>
</style>
</resources>

View File

@ -16,23 +16,16 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/pcap_dump" app:iconSpaceReserved="false">
<ListPreference
app:key="pcap_dump_mode"
app:title="@string/dump_mode"
app:iconSpaceReserved="false"
app:summary="@string/no_dump_info"
app:defaultValue="http_server"
app:entries="@array/pcap_dump_modes_labels"
app:entryValues="@array/pcap_dump_modes"/>
<PreferenceCategory app:title="@string/http_server" app:iconSpaceReserved="false">
<EditTextPreference
app:key="http_server_port"
app:title="@string/http_server_port"
app:defaultValue="8080"
app:iconSpaceReserved="false"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/udp_exporter" app:iconSpaceReserved="false">
<EditTextPreference
app:key="collector_ip_address"
app:title="@string/receiver_ip_address"