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" /> + +