From cff35c7e8a32709ac462aa4ffa048a13c50e5a18 Mon Sep 17 00:00:00 2001 From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:03:06 +0200 Subject: [PATCH 1/2] feat: Add split tunneling --- app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 1 + .../client/ui/advanced/AdvancedFragment.java | 7 + .../client/ui/splittunneling/AppInfo.java | 17 +++ .../splittunneling/SplitTunnelingAdapter.java | 87 ++++++++++++ .../SplitTunnelingFragment.java | 131 ++++++++++++++++++ .../main/res/drawable/ic_chevron_right.xml | 9 ++ app/src/main/res/layout/fragment_advanced.xml | 59 +++++++- .../res/layout/fragment_split_tunneling.xml | 88 ++++++++++++ app/src/main/res/layout/list_item_app.xml | 47 +++++++ .../main/res/navigation/mobile_navigation.xml | 6 + app/src/main/res/values/strings.xml | 7 + gradle/libs.versions.toml | 8 ++ .../java/io/netbird/client/tool/IFace.java | 41 +++++- .../io/netbird/client/tool/Preferences.java | 34 +++++ 15 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java create mode 100644 app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java create mode 100644 app/src/main/res/drawable/ic_chevron_right.xml create mode 100644 app/src/main/res/layout/fragment_split_tunneling.xml create mode 100644 app/src/main/res/layout/list_item_app.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index febc2632..906c86ad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,8 @@ android { dependencies { implementation(project(":tool")) implementation(project(":gomobile")) + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.ui) implementation(libs.appcompat) implementation(libs.material) implementation(libs.constraintlayout) @@ -78,6 +80,8 @@ dependencies { implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.navigation.fragment) implementation(libs.navigation.ui) + implementation("androidx.fragment:fragment:1.8.5") + implementation("androidx.recyclerview:recyclerview:1.3.2") testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 897d5fdf..2a4e55a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + { + NavController navController = Navigation.findNavController(v); + navController.navigate(R.id.nav_split_tunneling); + }); + return root; } diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java b/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java new file mode 100644 index 00000000..97e5e75e --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java @@ -0,0 +1,17 @@ +package io.netbird.client.ui.splittunneling; + +import android.graphics.drawable.Drawable; + +public class AppInfo { + public final String name; + public final String packageName; + public final Drawable icon; + public boolean isSelected; + + public AppInfo(String name, String packageName, Drawable icon, boolean isSelected) { + this.name = name; + this.packageName = packageName; + this.icon = icon; + this.isSelected = isSelected; + } +} diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java new file mode 100644 index 00000000..0c029caf --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingAdapter.java @@ -0,0 +1,87 @@ +package io.netbird.client.ui.splittunneling; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import io.netbird.client.R; + +public class SplitTunnelingAdapter extends RecyclerView.Adapter { + + private List allApps = new ArrayList<>(); + private List filteredApps = new ArrayList<>(); + private final OnAppClickListener listener; + + public interface OnAppClickListener { + void onAppClick(AppInfo app); + } + + public SplitTunnelingAdapter(OnAppClickListener listener) { + this.listener = listener; + } + + public void setApps(List apps) { + this.allApps = apps; + this.filteredApps = new ArrayList<>(apps); + notifyDataSetChanged(); + } + + public void filter(String query) { + if (query.isEmpty()) { + filteredApps = new ArrayList<>(allApps); + } else { + String lowerQuery = query.toLowerCase(); + filteredApps = allApps.stream() + .filter(app -> app.name.toLowerCase().contains(lowerQuery) || app.packageName.toLowerCase().contains(lowerQuery)) + .collect(Collectors.toList()); + } + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AppInfo app = filteredApps.get(position); + holder.textAppName.setText(app.name); + holder.textPackageName.setText(app.packageName); + holder.imgAppIcon.setImageDrawable(app.icon); + holder.checkApp.setChecked(app.isSelected); + holder.itemView.setOnClickListener(v -> listener.onAppClick(app)); + } + + @Override + public int getItemCount() { + return filteredApps.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + ImageView imgAppIcon; + TextView textAppName; + TextView textPackageName; + CheckBox checkApp; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + imgAppIcon = itemView.findViewById(R.id.img_app_icon); + textAppName = itemView.findViewById(R.id.text_app_name); + textPackageName = itemView.findViewById(R.id.text_package_name); + checkApp = itemView.findViewById(R.id.check_app); + } + } +} diff --git a/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java new file mode 100644 index 00000000..32d80665 --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/splittunneling/SplitTunnelingFragment.java @@ -0,0 +1,131 @@ +package io.netbird.client.ui.splittunneling; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.netbird.client.R; +import io.netbird.client.databinding.FragmentSplitTunnelingBinding; +import io.netbird.client.tool.Preferences; +import io.netbird.client.tool.Preferences.SplitTunnelingMode; + +public class SplitTunnelingFragment extends Fragment { + + private FragmentSplitTunnelingBinding binding; + private SplitTunnelingAdapter adapter; + private Preferences preferences; + private List allApps = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = FragmentSplitTunnelingBinding.inflate(inflater, container, false); + preferences = new Preferences(requireContext()); + setupUI(); + loadApps(); + return binding.getRoot(); + } + + private void setupUI() { + adapter = new SplitTunnelingAdapter(app -> { + app.isSelected = !app.isSelected; + saveApps(); + adapter.notifyDataSetChanged(); + }); + + binding.recyclerApps.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.recyclerApps.setAdapter(adapter); + + Preferences.SplitTunnelingMode mode = preferences.getSplitTunnelingMode(); + switch (mode) { + case NONE: + binding.radioGroupMode.check(R.id.radio_mode_none); + break; + case EXCLUDE: + binding.radioGroupMode.check(R.id.radio_mode_exclude); + break; + case INCLUDE: + binding.radioGroupMode.check(R.id.radio_mode_include); + break; + } + + binding.radioGroupMode.setOnCheckedChangeListener((group, checkedId) -> { + Preferences.SplitTunnelingMode newMode; + if (checkedId == R.id.radio_mode_none) { + newMode = Preferences.SplitTunnelingMode.NONE; + } else if (checkedId == R.id.radio_mode_exclude) { + newMode = Preferences.SplitTunnelingMode.EXCLUDE; + } else if (checkedId == R.id.radio_mode_include) { + newMode = Preferences.SplitTunnelingMode.INCLUDE; + } else { + newMode = Preferences.SplitTunnelingMode.NONE; + } + preferences.setSplitTunnelingMode(newMode); + }); + + binding.editSearch.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + adapter.filter(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) {} + }); + } + + private void loadApps() { + PackageManager pm = requireContext().getPackageManager(); + List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA); + Set selectedApps = preferences.getSplitTunnelingApps(); + String myPackageName = requireContext().getPackageName(); + + allApps = apps.stream() + .filter(app -> !app.packageName.equals(myPackageName)) + .map(app -> { + String name = pm.getApplicationLabel(app).toString(); + Drawable icon = pm.getApplicationIcon(app); + return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName)); + }) + .sorted((a, b) -> a.name.compareToIgnoreCase(b.name)) + .collect(Collectors.toList()); + + adapter.setApps(allApps); + } + + private void saveApps() { + Set selectedApps = allApps.stream() + .filter(app -> app.isSelected) + .map(app -> app.packageName) + .collect(Collectors.toSet()); + preferences.setSplitTunnelingApps(selectedApps); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 00000000..e28524ed --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_advanced.xml b/app/src/main/res/layout/fragment_advanced.xml index ce48fdf5..362fd6b6 100644 --- a/app/src/main/res/layout/fragment_advanced.xml +++ b/app/src/main/res/layout/fragment_advanced.xml @@ -523,6 +523,63 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/layout_disable_ipv6" /> + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/layout_split_tunneling"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_app.xml b/app/src/main/res/layout/list_item_app.xml new file mode 100644 index 00000000..0e6c749b --- /dev/null +++ b/app/src/main/res/layout/list_item_app.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 3f749393..b4e6e5c1 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -36,6 +36,12 @@ android:label="@string/menu_profiles" tools:layout="@layout/fragment_profiles" /> + + Choose the app appearance mode. Force relay connection Forces usage of relay when connecting to peers + Split Tunneling + Select which apps should use or bypass the VPN + Split Tunneling + None (All apps use VPN) + Exclude (Selected apps bypass VPN) + Include (Selected apps use VPN) + Search apps exclamation mark To apply the setting, you will need to reconnect. Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 018bc26c..7dcb8cbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,8 @@ appcompat = "1.7.0" lottie = "6.4.0" material = "1.12.0" constraintlayout = "2.2.1" +androidx-recyclerview = "1.3.2" +androidx-fragment = "1.8.5" lifecycleLivedataKtx = "2.9.0" lifecycleViewmodelKtx = "2.9.0" navigationFragment = "2.9.0" @@ -18,6 +20,8 @@ firebase-bom = "34.2.0" google-services = "4.4.2" firebase-crashlytics-plugin = "3.0.6" zxing = "4.3.0" +navigationFragmentVersion = "2.9.8" +navigationUiVersion = "2.9.8" [libraries] browser = { module = "androidx.browser:browser", version.ref = "browser" } @@ -30,6 +34,8 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview" } +androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-fragment" } lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" } lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" } @@ -38,6 +44,8 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } zxing = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing" } +androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragmentVersion" } +androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUiVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index 137fe8ce..fa683c63 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -12,6 +12,7 @@ import android.util.Log; import java.util.LinkedList; +import java.util.Set; import java.util.concurrent.CountDownLatch; import io.netbird.gomobile.android.TunAdapter; @@ -86,10 +87,7 @@ private int createTun(String ip, int prefixLength, InetNetwork addrV6, int mtu, Log.d(LOGTAG, "add route: "+r.addr+"/"+r.prefixLength); } - disallowApp(builder, "com.google.android.projection.gearhead"); - disallowApp(builder, "com.google.android.apps.chromecast.app"); - disallowApp(builder, "com.google.android.apps.messaging"); - disallowApp(builder, "com.google.stadia.android"); + applySplitTunneling(builder); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { builder.setMetered(false); @@ -145,6 +143,41 @@ private void disallowApp(VpnService.Builder builder, String packageName) { } } + private void applySplitTunneling(VpnService.Builder builder) { + Preferences prefs = new Preferences(vpnService); + Preferences.SplitTunnelingMode mode = prefs.getSplitTunnelingMode(); + Set apps = prefs.getSplitTunnelingApps(); + + Log.d(LOGTAG, "Applying split tunneling mode: " + mode); + + if (mode == Preferences.SplitTunnelingMode.INCLUDE) { + if (apps.isEmpty()) { + Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively disabled for all apps"); + } + for (String app : apps) { + try { + builder.addAllowedApplication(app); + Log.d(LOGTAG, "Allowed app: " + app); + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOGTAG, "App not found for allowed list: " + app); + } + } + } else { + // Default and EXCLUDE mode + disallowApp(builder, "com.google.android.projection.gearhead"); + disallowApp(builder, "com.google.android.apps.chromecast.app"); + disallowApp(builder, "com.google.android.apps.messaging"); + disallowApp(builder, "com.google.stadia.android"); + + if (mode == Preferences.SplitTunnelingMode.EXCLUDE) { + for (String app : apps) { + disallowApp(builder, app); + Log.d(LOGTAG, "Disallowed app: " + app); + } + } + } + } + @SuppressLint("DefaultLocale") @Override public void updateAddr(String s) throws Exception { diff --git a/tool/src/main/java/io/netbird/client/tool/Preferences.java b/tool/src/main/java/io/netbird/client/tool/Preferences.java index ddf57ca5..e49d9655 100644 --- a/tool/src/main/java/io/netbird/client/tool/Preferences.java +++ b/tool/src/main/java/io/netbird/client/tool/Preferences.java @@ -3,12 +3,19 @@ import android.content.Context; import android.content.SharedPreferences; +import java.util.HashSet; +import java.util.Set; + public class Preferences { private final String keyTraceLog = "tracelog"; private final String keyForceRelayConnection = "isConnectionForceRelayed"; + private final String keySplitTunnelingMode = "splitTunnelingMode"; + + private final String keySplitTunnelingApps = "splitTunnelingApps"; + private final SharedPreferences sharedPref; public Preferences(Context context) { @@ -41,4 +48,31 @@ public void disableForcedRelayConnection() { public static String defaultServer() { return "https://api.netbird.io"; } + + public enum SplitTunnelingMode { + NONE, + EXCLUDE, + INCLUDE + } + + public SplitTunnelingMode getSplitTunnelingMode() { + String mode = sharedPref.getString(keySplitTunnelingMode, SplitTunnelingMode.NONE.name()); + try { + return SplitTunnelingMode.valueOf(mode); + } catch (IllegalArgumentException e) { + return SplitTunnelingMode.NONE; + } + } + + public void setSplitTunnelingMode(SplitTunnelingMode mode) { + sharedPref.edit().putString(keySplitTunnelingMode, mode.name()).apply(); + } + + public Set getSplitTunnelingApps() { + return sharedPref.getStringSet(keySplitTunnelingApps, new HashSet<>()); + } + + public void setSplitTunnelingApps(Set apps) { + sharedPref.edit().putStringSet(keySplitTunnelingApps, apps).apply(); + } } From da271e94b8824825e268be5f49f70c2b9ba06a8b Mon Sep 17 00:00:00 2001 From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:49:01 +0200 Subject: [PATCH 2/2] fix: apply code rabbit feedback --- app/build.gradle.kts | 6 +- app/src/main/AndroidManifest.xml | 15 +++- .../SplitTunnelingFragment.java | 72 +++++++++++++------ .../main/res/drawable/ic_chevron_right.xml | 3 +- .../res/layout/fragment_split_tunneling.xml | 10 +++ .../java/io/netbird/client/tool/IFace.java | 2 +- 6 files changed, 79 insertions(+), 29 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 906c86ad..14273628 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -78,10 +78,8 @@ dependencies { implementation(libs.constraintlayout) implementation(libs.lifecycle.livedata.ktx) implementation(libs.lifecycle.viewmodel.ktx) - implementation(libs.navigation.fragment) - implementation(libs.navigation.ui) - implementation("androidx.fragment:fragment:1.8.5") - implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation(libs.androidx.fragment) + implementation(libs.androidx.recyclerview) testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2a4e55a3..5e8076da 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ - + + + + + + + + + + + + + + allApps = new ArrayList<>(); + private ExecutorService backgroundExecutor; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentSplitTunnelingBinding.inflate(inflater, container, false); preferences = new Preferences(requireContext()); + backgroundExecutor = Executors.newSingleThreadExecutor(); setupUI(); loadApps(); return binding.getRoot(); @@ -97,35 +101,59 @@ public void afterTextChanged(Editable s) {} } private void loadApps() { - PackageManager pm = requireContext().getPackageManager(); - List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA); - Set selectedApps = preferences.getSplitTunnelingApps(); - String myPackageName = requireContext().getPackageName(); - - allApps = apps.stream() - .filter(app -> !app.packageName.equals(myPackageName)) - .map(app -> { - String name = pm.getApplicationLabel(app).toString(); - Drawable icon = pm.getApplicationIcon(app); - return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName)); - }) - .sorted((a, b) -> a.name.compareToIgnoreCase(b.name)) - .collect(Collectors.toList()); - - adapter.setApps(allApps); + if (backgroundExecutor == null) return; + Context context = getContext(); + if (context == null) return; + + PackageManager pm = context.getPackageManager(); + String myPackageName = context.getPackageName(); + binding.progressBar.setVisibility(View.VISIBLE); + + backgroundExecutor.execute(() -> { + List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA); + Set selectedApps = preferences.getSplitTunnelingApps(); + + List loadedApps = apps.stream() + .filter(app -> !app.packageName.equals(myPackageName)) + .map(app -> { + String name = pm.getApplicationLabel(app).toString(); + Drawable icon = pm.getApplicationIcon(app); + return new AppInfo(name, app.packageName, icon, selectedApps.contains(app.packageName)); + }) + .sorted((a, b) -> a.name.compareToIgnoreCase(b.name)) + .collect(Collectors.toList()); + + if (binding != null) { + binding.getRoot().post(() -> { + if (binding != null) { + allApps = loadedApps; + adapter.setApps(allApps); + binding.progressBar.setVisibility(View.GONE); + } + }); + } + }); } private void saveApps() { - Set selectedApps = allApps.stream() - .filter(app -> app.isSelected) - .map(app -> app.packageName) - .collect(Collectors.toSet()); - preferences.setSplitTunnelingApps(selectedApps); + if (backgroundExecutor == null) return; + List appsSnapshot = new ArrayList<>(allApps); + backgroundExecutor.execute(() -> { + Set selectedApps = appsSnapshot.stream() + .filter(app -> app.isSelected) + .map(app -> app.packageName) + .collect(Collectors.toSet()); + preferences.setSplitTunnelingApps(selectedApps); + }); } @Override public void onDestroyView() { super.onDestroyView(); + if (backgroundExecutor != null) { + backgroundExecutor.shutdown(); + backgroundExecutor = null; + } binding = null; } } diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml index e28524ed..c55f00c0 100644 --- a/app/src/main/res/drawable/ic_chevron_right.xml +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> diff --git a/app/src/main/res/layout/fragment_split_tunneling.xml b/app/src/main/res/layout/fragment_split_tunneling.xml index 836bb453..5f5f2593 100644 --- a/app/src/main/res/layout/fragment_split_tunneling.xml +++ b/app/src/main/res/layout/fragment_split_tunneling.xml @@ -83,6 +83,16 @@ app:layout_constraintEnd_toEndOf="parent" tools:listitem="@layout/list_item_app" /> + + diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index fa683c63..1401d1b3 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -152,7 +152,7 @@ private void applySplitTunneling(VpnService.Builder builder) { if (mode == Preferences.SplitTunnelingMode.INCLUDE) { if (apps.isEmpty()) { - Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively disabled for all apps"); + Log.w(LOGTAG, "Include mode selected but no apps specified, VPN will be effectively enabled for all apps"); } for (String app : apps) { try {