diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3fe95f1f..95e13b65 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -79,6 +79,10 @@
android:name=".activities.SettingsActivity"
android:launchMode="singleTop"
android:parentActivityName=".activities.MainActivity" />
+
exceptions = mPrefs.getStringSet(Prefs.PREF_VPN_EXCEPTIONS, new HashSet<>());
+ for(String packageName: exceptions) {
+ try {
+ builder.addDisallowedApplication(packageName);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if(mSettings.tls_decryption) {
+ // Exclude the mitm addon traffic in case system-wide decryption is performed
+ // Important: cannot call addDisallowedApplication with addAllowedApplication
+ try {
+ builder.addDisallowedApplication(MitmAPI.PACKAGE_NAME);
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
}
}
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 b7cefa9f..4d0fcfc0 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
@@ -79,9 +79,11 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
@Override
public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, @NonNull Preference pref) {
PreferenceFragmentCompat targetFragment = null;
- Log.d(TAG, "startFragment: " + pref.getKey());
+ String prefKey = pref.getKey();
- if(pref.getKey().equals("geolocation")) {
+ Log.d(TAG, "startFragment: " + prefKey);
+
+ if(prefKey.equals("geolocation")) {
targetFragment = new GeoipSettings();
setTitle(R.string.geolocation);
}
@@ -130,6 +132,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
private EditTextPreference mSocks5ProxyPort;
private DropDownPreference mIpMode;
private DropDownPreference mCapInterface;
+ private Preference mVpnExceptions;
private SwitchPreference mMalwareDetectionEnabled;
private Billing mIab;
private boolean mHasStartedMitmWizard;
@@ -246,7 +249,14 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
private void setupCapturePrefs() {
mCapInterface = requirePreference(Prefs.PREF_CAPTURE_INTERFACE);
+ mVpnExceptions = requirePreference(Prefs.PREF_VPN_EXCEPTIONS);
refreshInterfaces();
+
+ mVpnExceptions.setOnPreferenceClickListener(preference -> {
+ Intent intent = new Intent(requireContext(), VpnExceptionsActivity.class);
+ startActivity(intent);
+ return true;
+ });
}
private void setupSecurityPrefs() {
@@ -391,6 +401,7 @@ public class SettingsActivity extends BaseActivity implements PreferenceFragment
mIpMode.setVisible(!enabled);
mCapInterface.setVisible(enabled);
+ mVpnExceptions.setVisible(!enabled);
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/VpnExceptionsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/VpnExceptionsActivity.java
new file mode 100644
index 00000000..a5177319
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/activities/VpnExceptionsActivity.java
@@ -0,0 +1,102 @@
+/*
+ * 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 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.activities;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.fragments.AppsToggles;
+import com.emanuelef.remote_capture.model.AppDescriptor;
+import com.emanuelef.remote_capture.model.Prefs;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class VpnExceptionsActivity extends BaseActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.vpn_exceptions);
+ setContentView(R.layout.fragment_activity);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment, new VpnExceptionsFragment())
+ .commit();
+ }
+
+ public static class VpnExceptionsFragment extends AppsToggles {
+ private static final String TAG = "VpnExceptions";
+ private final Set mExcludedApps = new HashSet<>();
+ private @Nullable SharedPreferences mPrefs;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ assert mPrefs != null;
+
+ mExcludedApps.clear();
+ Set saved = mPrefs.getStringSet(Prefs.PREF_VPN_EXCEPTIONS, null);
+ if(saved != null) {
+ Log.d(TAG, "Loading " + saved.size() + " exceptions");
+ mExcludedApps.addAll(saved);
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mPrefs = null;
+ }
+
+ @Override
+ protected Set getCheckedApps() {
+ return mExcludedApps;
+ }
+
+ @Override
+ public void onAppToggled(AppDescriptor app, boolean checked) {
+ String packageName = app.getPackageName();
+ if(mExcludedApps.contains(packageName) == checked)
+ return; // nothing to do
+
+ if(checked)
+ mExcludedApps.add(packageName);
+ else
+ mExcludedApps.remove(packageName);
+
+ Log.d(TAG, "Saving " + mExcludedApps.size() + " exceptions");
+
+ if(mPrefs == null)
+ return;
+
+ mPrefs.edit()
+ .putStringSet(Prefs.PREF_VPN_EXCEPTIONS, mExcludedApps)
+ .apply();
+ }
+ }
+}
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 690053fc..2998d2e2 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
@@ -53,7 +53,7 @@ public class AppsAdapter extends RecyclerView.Adapter
public AppViewHolder(View view) {
super(view);
- textInListView = view.findViewById(R.id.list_app_name);
+ textInListView = view.findViewById(R.id.app_name);
imageInListView = view.findViewById(R.id.app_icon);
packageInListView= view.findViewById(R.id.app_package);
}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsTogglesAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsTogglesAdapter.java
new file mode 100644
index 00000000..278845b5
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/AppsTogglesAdapter.java
@@ -0,0 +1,209 @@
+/*
+ * 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 2020-21 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.adapters;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.model.AppDescriptor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class AppsTogglesAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "AppToggleAdapter";
+ private final LayoutInflater mLayoutInflater;
+ private final Set mCheckedItems;
+ private AppToggleListener mListener;
+ private List mApps = new ArrayList<>();
+ private @Nullable RecyclerView mRecyclerView;
+
+ public AppsTogglesAdapter(Context context, Set checkedItems) {
+ mLayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mCheckedItems = new HashSet<>(checkedItems);
+ mListener = null;
+ }
+
+ public interface AppToggleListener {
+ void onAppToggled(AppDescriptor app, boolean checked);
+ }
+
+ public static class AppViewHolder extends RecyclerView.ViewHolder {
+ TextView appName;
+ TextView packageName;
+ ImageView icon;
+ SwitchCompat toggle;
+
+ public AppViewHolder(View view) {
+ super(view);
+
+ appName = view.findViewById(R.id.app_name);
+ icon = view.findViewById(R.id.icon);
+ packageName = view.findViewById(R.id.app_package);
+ toggle = view.findViewById(R.id.toggle_btn);
+ }
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView);
+ mRecyclerView = null;
+ }
+
+ @NonNull
+ @Override
+ public AppsTogglesAdapter.AppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = mLayoutInflater.inflate(R.layout.app_selection_item, parent, false);
+ AppViewHolder recyclerViewHolder = new AppViewHolder(view);
+
+ view.setOnClickListener((v) -> {
+ if(mRecyclerView != null) {
+ int pos = recyclerViewHolder.getAbsoluteAdapterPosition();
+ boolean checked = mCheckedItems.contains(getItem(pos).getPackageName());
+ handleToggle(pos, !checked);
+ }
+ });
+
+ recyclerViewHolder.toggle.setOnClickListener((v) -> {
+ if(mRecyclerView != null) {
+ int pos = recyclerViewHolder.getAbsoluteAdapterPosition();
+ boolean checked = ((SwitchCompat)v).isChecked();
+ handleToggle(pos, checked);
+ }
+ });
+
+ return(recyclerViewHolder);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AppViewHolder holder, int position) {
+ AppDescriptor app = getItem(position);
+
+ holder.appName.setText(app.getName());
+ holder.packageName.setText(app.getPackageName());
+ holder.toggle.setChecked(mCheckedItems.contains(app.getPackageName()));
+
+ if(app.getIcon() != null)
+ holder.icon.setImageDrawable(app.getIcon());
+ }
+
+ @Override
+ public int getItemCount() {
+ return mApps.size();
+ }
+
+ public AppDescriptor getItem(int pos) {
+ if((pos < 0) || (pos > mApps.size()))
+ return null;
+
+ return mApps.get(pos);
+ }
+
+ private void handleToggle(int old_pos, boolean checked) {
+ AppDescriptor app = getItem(old_pos);
+ String packageName = app.getPackageName();
+
+ if(checked == mCheckedItems.contains(packageName))
+ return; // nothing changed
+
+ if(checked)
+ mCheckedItems.add(packageName);
+ else
+ mCheckedItems.remove(packageName);
+
+ if(mListener != null)
+ mListener.onAppToggled(app, checked);
+
+ // determine the new item position
+ int new_pos = old_pos;
+ for(int i=0; i old_pos)
+ new_pos--;
+
+ Log.d(TAG, "Item @" + old_pos + ": " + (checked ? "checked" : "unchecked") + " -> " + new_pos);
+ notifyItemChanged(old_pos);
+
+ if(new_pos != old_pos) {
+ mApps.remove(old_pos);
+ mApps.add(new_pos, app);
+ notifyItemMoved(old_pos, new_pos);
+
+ if(mRecyclerView != null) {
+ if(checked)
+ mRecyclerView.scrollToPosition(new_pos);
+ else
+ mRecyclerView.scrollToPosition(old_pos);
+ }
+ }
+ }
+
+ // sort apps so that checked items always appear first
+ private int compareCheckedFirst(AppDescriptor a, AppDescriptor b) {
+ boolean aChecked = mCheckedItems.contains(a.getPackageName());
+ boolean bChecked = mCheckedItems.contains(b.getPackageName());
+
+ if(aChecked && !bChecked)
+ return -1;
+ else if(!aChecked && bChecked)
+ return 1;
+ return a.compareTo(b);
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public void setApps(List apps) {
+ mApps = apps;
+ Collections.sort(mApps, this::compareCheckedFirst);
+
+ notifyDataSetChanged();
+ }
+
+ public void setAppToggleListener(final AppToggleListener listener) {
+ mListener = listener;
+ }
+}
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java
index 461bbee8..dadab509 100644
--- a/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsFragment.java
@@ -40,7 +40,6 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import com.emanuelef.remote_capture.Billing;
import com.emanuelef.remote_capture.CaptureService;
import com.emanuelef.remote_capture.ConnectionsRegister;
import com.emanuelef.remote_capture.PCAPdroid;
@@ -84,7 +83,7 @@ public class AppsFragment extends Fragment implements ConnectionsListener {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- mRecyclerView = view.findViewById(R.id.apps_stats_view);
+ mRecyclerView = view.findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new EmptyRecyclerView.MyLinearLayoutManager(getContext()));
registerForContextMenu(mRecyclerView);
diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsToggles.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsToggles.java
new file mode 100644
index 00000000..d7523b5e
--- /dev/null
+++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/AppsToggles.java
@@ -0,0 +1,82 @@
+/*
+ * 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 2020-22 - Emanuele Faranda
+ */
+
+package com.emanuelef.remote_capture.fragments;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+
+import com.emanuelef.remote_capture.AppsLoader;
+import com.emanuelef.remote_capture.R;
+import com.emanuelef.remote_capture.adapters.AppsTogglesAdapter;
+import com.emanuelef.remote_capture.interfaces.AppsLoadListener;
+import com.emanuelef.remote_capture.model.AppDescriptor;
+import com.emanuelef.remote_capture.views.EmptyRecyclerView;
+
+import java.util.List;
+import java.util.Set;
+
+import kotlin.NotImplementedError;
+
+public abstract class AppsToggles extends Fragment implements AppsLoadListener, AppsTogglesAdapter.AppToggleListener {
+ private AppsTogglesAdapter mAdapter;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+ return inflater.inflate(R.layout.apps_stats, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ EmptyRecyclerView recyclerView = view.findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new EmptyRecyclerView.MyLinearLayoutManager(getContext()));
+
+ mAdapter = new AppsTogglesAdapter(requireContext(), getCheckedApps());
+ recyclerView.setAdapter(mAdapter);
+ mAdapter.setAppToggleListener(this);
+
+ TextView emptyAppsView = view.findViewById(R.id.no_apps);
+ emptyAppsView.setText(R.string.loading_apps);
+ recyclerView.setEmptyView(emptyAppsView);
+
+ (new AppsLoader((AppCompatActivity) requireActivity()))
+ .setAppsLoadListener(this)
+ .loadAllApps();
+ }
+
+ @Override
+ public void onAppsInfoLoaded(List apps) {
+ mAdapter.setApps(apps);
+ }
+
+ // Must be implemented in sub-classes
+ protected Set getCheckedApps() {
+ throw new NotImplementedError();
+ }
+}
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 e00732f1..2d78e164 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
@@ -74,6 +74,7 @@ public class Prefs {
public static final String PREF_AUTO_BLOCK_PRIVATE_DNS = "auto_block_private_dns";
public static final String PREF_APP_VERSION = "appver";
public static final String PREF_LOCKDOWN_VPN_NOTICE_SHOWN = "vpn_lockdown_notice";
+ public static final String PREF_VPN_EXCEPTIONS = "vpn_exceptions";
public enum DumpMode {
NONE,
diff --git a/app/src/main/res/layout/app_installed_item.xml b/app/src/main/res/layout/app_installed_item.xml
index 9452cf78..3aeb67ff 100644
--- a/app/src/main/res/layout/app_installed_item.xml
+++ b/app/src/main/res/layout/app_installed_item.xml
@@ -1,5 +1,6 @@
+ tools:text="App Name"/>
diff --git a/app/src/main/res/layout/app_selection_item.xml b/app/src/main/res/layout/app_selection_item.xml
new file mode 100644
index 00000000..f0962b5c
--- /dev/null
+++ b/app/src/main/res/layout/app_selection_item.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/apps_stats.xml b/app/src/main/res/layout/apps_stats.xml
index 02222a30..b3cb4dd2 100644
--- a/app/src/main/res/layout/apps_stats.xml
+++ b/app/src/main/res/layout/apps_stats.xml
@@ -4,7 +4,7 @@
android:layout_height="match_parent">
IPv6 only
IPv4 and IPv6
The app uses notifications to send alerts in case of anomalous events. Grant it the permission to send notifications in the next screen
+ VPN Exceptions
+ Exclude some apps from the VPN connection. Their traffic will not be monitored
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index d1a730a5..e0ef2db5 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -117,6 +117,13 @@
app:useSimpleSummaryProvider="true"
app:defaultValue="\@inet" />
+
+