diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index 7339fbc6..70fe6b55 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -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 { diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java index 01f942c5..56038697 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -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 appsData, AppsListView.OnSelectedAppListener listener) { + public static Dialog getAppSelectionDialog(Activity activity, List 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); + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java index c8ed6578..b586d381 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java @@ -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 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 apps) { - // TODO optimize: show the apps even when icon not loaded - } - - @Override - public void onAppsIconsLoaded(Map apps) { - mInstalledApps = new ArrayList<>(); - AppDescriptor filterApp = null; - - for (Map.Entry 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; } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java index d97b81fc..ceb15cd9 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java @@ -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); diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsAdapter.java index 31d9e389..82818409 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsAdapter.java @@ -76,8 +76,10 @@ public class AppsAdapter extends RecyclerView.Adapter 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 diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/DumpModesAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/DumpModesAdapter.java new file mode 100644 index 00000000..7146808f --- /dev/null +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/DumpModesAdapter.java @@ -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 setUidFilter(app.getUid())); + Utils.getAppSelectionDialog(getActivity(), appsData, app -> setUidFilter(app.getUid())).show(); } private void setUidFilter(int uid) { diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java index fb5248b5..ab612efe 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java @@ -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 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 apps, boolean with_icons) { + mInstalledApps = new ArrayList<>(); + + for(Map.Entry 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 apps) { + loadInstalledApps(apps, false); + } + + @Override + public void onAppsIconsLoaded(Map apps) { + loadInstalledApps(apps, true); + + // Possibly update the icons + if(mOpenAppsList != null) { + Log.d(TAG, "reloading app icons in dialog"); + mOpenAppsList.setApps(mInstalledApps); + } + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java index d02ac85d..7fec28ed 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java @@ -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, "")); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/views/AppsListView.java b/app/src/main/java/com/emanuelef/remote_capture/views/AppsListView.java index 5452dab1..84a307f3 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/views/AppsListView.java +++ b/app/src/main/java/com/emanuelef/remote_capture/views/AppsListView.java @@ -113,8 +113,12 @@ public class AppsListView extends EmptyRecyclerView implements SearchView.OnQuer public void setApps(List 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) { diff --git a/app/src/main/res/layout/app_bar.xml b/app/src/main/res/layout/app_bar.xml index 0a670769..10645446 100644 --- a/app/src/main/res/layout/app_bar.xml +++ b/app/src/main/res/layout/app_bar.xml @@ -23,12 +23,17 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" > + + + \ No newline at end of file diff --git a/app/src/main/res/layout/quick_settings_item.xml b/app/src/main/res/layout/quick_settings_item.xml new file mode 100644 index 00000000..ff0bbfcd --- /dev/null +++ b/app/src/main/res/layout/quick_settings_item.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/status.xml b/app/src/main/res/layout/status.xml index 48a6aff2..80a0228d 100644 --- a/app/src/main/res/layout/status.xml +++ b/app/src/main/res/layout/status.xml @@ -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 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 599ad77e..2c925f16 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -9,13 +9,6 @@ android:icon="@android:drawable/ic_media_play" app:showAsAction="always" /> - - @string/pcap_file @string/udp_exporter + + + @string/no_dump_info + @string/http_server_info + @string/pcap_file_info + @string/udp_exporter_info + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae6ebe4c..0200e7b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,8 +47,8 @@ Start an HTTP server for the PCAP download Sends the PCAP to a remote UDP receiver HTTP Server Port - Receiver IP Address - Receiver Port + Collector IP Address + Collector Port Dump Mode mitmproxy IP Address mitmproxy Port @@ -99,5 +99,6 @@ Could not delete file Capture running %1$s captured, %2$s connections + No app filter set diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 6efcd76e..c4e6f10f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,4 +17,10 @@ + diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 30ff5df3..409b8ac7 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -16,23 +16,16 @@ - - - + + +