Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
android:name="io.netbird.permission.NOTIFICATION"
android:protectionLevel="signature" />

<!--
Split tunnelling needs to show the user a list of apps. Resolving the
launcher intent keeps that to apps with an icon, which avoids
QUERY_ALL_PACKAGES and the Play declaration it drags along.
-->
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/io/netbird/client/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,17 @@ public NetworkArray getNetworks() {
return mBinder.networks();
}

@Override
public void applySplitTunneling() {
if (mBinder == null) {
// Nothing is running to rebuild; the new selection is read when the
// tunnel is next created.
return;
}

mBinder.applySplitTunneling();
}

@Override
public void selectRoute(String route) throws Exception {
if (mBinder == null) {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/io/netbird/client/ServiceAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public interface ServiceAccessor {

void stopEngine();

/** Rebuilds the tunnel so a split tunnelling change applies without reconnecting. */
void applySplitTunneling();

void selectRoute(String route) throws Exception;
void deselectRoute(String route) throws Exception;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
binding.rowAdvanced.setOnClickListener(v ->
navController.navigate(R.id.nav_advanced));

binding.rowSplitTunneling.setOnClickListener(v ->
navController.navigate(R.id.nav_split_tunneling));

binding.rowLanguage.setOnClickListener(v ->
new LanguagePickerSheet().show(getChildFragmentManager(), "language_picker"));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.netbird.client.ui.splittunneling;

import android.graphics.drawable.Drawable;

/** One installed application, as shown in the split tunnelling list. */
public class AppEntry {

private final String packageName;
private final String label;
private final Drawable icon;

public AppEntry(String packageName, String label, Drawable icon) {
this.packageName = packageName;
this.label = label;
this.icon = icon;
}

public String getPackageName() {
return packageName;
}

public String getLabel() {
return label;
}

public Drawable getIcon() {
return icon;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.netbird.client.ui.splittunneling;

import android.view.LayoutInflater;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import io.netbird.client.databinding.ListItemAppBinding;

public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.AppViewHolder> {

public interface OnAppToggledListener {
void onAppToggled(String packageName, boolean selected);
}

private final List<AppEntry> apps = new ArrayList<>();
private final List<AppEntry> filteredApps = new ArrayList<>();
private final OnAppToggledListener toggleListener;

private Set<String> selected;
private String filterQueryString = "";

public AppListAdapter(Set<String> selected, OnAppToggledListener toggleListener) {
this.selected = selected;
this.toggleListener = toggleListener;
}

public void submitApps(List<AppEntry> newApps) {
apps.clear();
apps.addAll(newApps);
applyFilter();
}

/** Called when the mode changes, which swaps which of the two lists is shown. */
public void setSelected(Set<String> selected) {
this.selected = selected;
notifyDataSetChanged();
}

public void filterBySearchQuery(String query) {
filterQueryString = query == null ? "" : query;
applyFilter();
}

private void applyFilter() {
filteredApps.clear();
if (filterQueryString.isEmpty()) {
filteredApps.addAll(apps);
} else {
String needle = filterQueryString.toLowerCase(Locale.getDefault());
for (AppEntry app : apps) {
if (app.getLabel().toLowerCase(Locale.getDefault()).contains(needle)) {
filteredApps.add(app);
}
}
}
notifyDataSetChanged();
}

@NonNull
@Override
public AppViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ListItemAppBinding binding = ListItemAppBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new AppViewHolder(binding);
}

@Override
public void onBindViewHolder(@NonNull AppViewHolder holder, int position) {
holder.bind(filteredApps.get(position));
}

@Override
public int getItemCount() {
return filteredApps.size();
}

class AppViewHolder extends RecyclerView.ViewHolder {

private final ListItemAppBinding binding;

AppViewHolder(ListItemAppBinding binding) {
super(binding.getRoot());
this.binding = binding;
}

void bind(AppEntry app) {
binding.appName.setText(app.getLabel());
binding.appPackage.setText(app.getPackageName());
binding.appIcon.setImageDrawable(app.getIcon());

// Cleared before setChecked so recycling a row into a different app
// cannot fire a toggle the user never made.
binding.switchControl.setOnCheckedChangeListener(null);
binding.switchControl.setChecked(selected.contains(app.getPackageName()));
binding.switchControl.setOnCheckedChangeListener((buttonView, isChecked) ->
toggleListener.onAppToggled(app.getPackageName(), isChecked));

binding.getRoot().setOnClickListener(v -> binding.switchControl.toggle());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.netbird.client.ui.splittunneling;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.android.material.bottomsheet.BottomSheetDialogFragment;

import io.netbird.client.databinding.SheetSplitTunnelModeBinding;
import io.netbird.client.tool.SplitTunnelConfig;

public class SplitTunnelModeSheet extends BottomSheetDialogFragment {

public interface OnModeChangedListener {
void onModeChanged(SplitTunnelConfig.Mode mode);
}

private static final String ARG_MODE = "mode";

private SheetSplitTunnelModeBinding binding;

public static SplitTunnelModeSheet newInstance(SplitTunnelConfig.Mode current) {
SplitTunnelModeSheet sheet = new SplitTunnelModeSheet();
Bundle args = new Bundle();
args.putString(ARG_MODE, current.name());
sheet.setArguments(args);
return sheet;
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = SheetSplitTunnelModeBinding.inflate(inflater, container, false);

binding.modeRowOff.setOnClickListener(v -> pick(SplitTunnelConfig.Mode.OFF));
binding.modeRowExclude.setOnClickListener(v -> pick(SplitTunnelConfig.Mode.EXCLUDE));
binding.modeRowInclude.setOnClickListener(v -> pick(SplitTunnelConfig.Mode.INCLUDE));

showCheckmarkFor(currentMode());
return binding.getRoot();
}

private SplitTunnelConfig.Mode currentMode() {
Bundle args = getArguments();
if (args == null) {
return SplitTunnelConfig.Mode.OFF;
}
try {
return SplitTunnelConfig.Mode.valueOf(args.getString(ARG_MODE, SplitTunnelConfig.Mode.OFF.name()));
} catch (IllegalArgumentException e) {
return SplitTunnelConfig.Mode.OFF;
}
}

private void showCheckmarkFor(SplitTunnelConfig.Mode mode) {
binding.modeCheckOff.setVisibility(mode == SplitTunnelConfig.Mode.OFF ? View.VISIBLE : View.INVISIBLE);
binding.modeCheckExclude.setVisibility(mode == SplitTunnelConfig.Mode.EXCLUDE ? View.VISIBLE : View.INVISIBLE);
binding.modeCheckInclude.setVisibility(mode == SplitTunnelConfig.Mode.INCLUDE ? View.VISIBLE : View.INVISIBLE);
}

private void pick(SplitTunnelConfig.Mode mode) {
if (getParentFragment() instanceof OnModeChangedListener) {
((OnModeChangedListener) getParentFragment()).onModeChanged(mode);
}
dismiss();
}

@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
Loading
Loading