Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@ 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)
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")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

<uses-feature
android:name="android.software.leanback"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;

import io.netbird.client.R;
import io.netbird.client.databinding.ComponentSwitchBinding;
Expand Down Expand Up @@ -185,6 +187,11 @@ public View onCreateView(@NonNull LayoutInflater inflater,
AppCompatDelegate.setDefaultNightMode(mode);
});

binding.layoutSplitTunneling.setOnClickListener(v -> {
NavController navController = Navigation.findNavController(v);
navController.navigate(R.id.nav_split_tunneling);
});

return root;
}

Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/io/netbird/client/ui/splittunneling/AppInfo.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<SplitTunnelingAdapter.ViewHolder> {

private List<AppInfo> allApps = new ArrayList<>();
private List<AppInfo> 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<AppInfo> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AppInfo> 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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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<ApplicationInfo> apps = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Set<String> 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<String> 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;
}
}
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_chevron_right.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8.59,16.59L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.59Z" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</vector>
59 changes: 58 additions & 1 deletion app/src/main/res/layout/fragment_advanced.xml
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,63 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_disable_ipv6" />

<View
android:id="@+id/separator_split_tunneling"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:background="@drawable/separator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_force_relay_connection" />

<LinearLayout
android:id="@+id/layout_split_tunneling"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/focus_highlight"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/separator_split_tunneling">

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/advanced_split_tunneling"
android:textColor="@color/nb_txt"
android:textSize="18sp"
android:textStyle="bold" />

<TextView
android:id="@+id/text_split_tunneling_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/advanced_split_tunneling_desc"
android:textColor="@color/nb_txt_light"
android:textSize="14sp" />
</LinearLayout>

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:srcCompat="@drawable/ic_chevron_right"
app:tint="@color/nb_txt_light" />
</LinearLayout>

<LinearLayout
android:id="@+id/layout_theme"
android:layout_width="0dp"
Expand All @@ -531,7 +588,7 @@
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layout_force_relay_connection">
app:layout_constraintTop_toBottomOf="@id/layout_split_tunneling">

<TextView
android:layout_width="wrap_content"
Expand Down
Loading