Skip to content

Commit 8112970

Browse files
committed
Implement example UI
1 parent 773b1fa commit 8112970

9 files changed

Lines changed: 402 additions & 30 deletions

File tree

android/vpnclient-engine-example/build.gradle

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ dependencies {
3333
implementation libs.appcompat
3434
implementation libs.material
3535
implementation libs.activity
36-
implementation libs.constraintlayout
37-
testImplementation libs.junit
38-
androidTestImplementation libs.ext.junit
39-
androidTestImplementation libs.espresso.core
36+
// implementation libs.constraintlayout
37+
implementation project(':vpnclient-engine')
38+
// testImplementation libs.junit
39+
// androidTestImplementation libs.ext.junit
40+
// androidTestImplementation libs.espresso.core
4041
}

android/vpnclient-engine-example/src/main/AndroidManifest.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<!-- VPN permissions -->
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.MANAGE_DEVICE_POLICY_VPN" />
7+
48
<application
59
android:allowBackup="true"
610
android:icon="@mipmap/ic_launcher"
711
android:label="@string/app_name"
812
android:roundIcon="@mipmap/ic_launcher_round"
913
android:supportsRtl="true"
10-
android:theme="@style/Theme.VPNClientengine">
14+
android:theme="@style/Theme.VPNClientEngine">
1115
<activity
1216
android:name=".MainActivity"
1317
android:exported="true">
@@ -17,6 +21,16 @@
1721
<category android:name="android.intent.category.LAUNCHER" />
1822
</intent-filter>
1923
</activity>
24+
25+
<!-- VPN Service declaration -->
26+
<service
27+
android:name=".VPNService"
28+
android:exported="false"
29+
android:permission="android.permission.BIND_VPN_SERVICE">
30+
<intent-filter>
31+
<action android:name="android.service.vpn.VpnService" />
32+
</intent-filter>
33+
</service>
2034
</application>
2135

2236
</manifest>
Lines changed: 177 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,191 @@
11
package click.vpnclient.engine.example;
22

3+
import android.content.Intent;
4+
import android.net.VpnService;
35
import android.os.Bundle;
6+
import android.util.Log;
7+
import android.widget.Button;
8+
import android.widget.TextView;
9+
import android.widget.Toast;
410

5-
import androidx.activity.EdgeToEdge;
11+
import androidx.activity.result.ActivityResultLauncher;
12+
import androidx.activity.result.contract.ActivityResultContracts;
613
import androidx.appcompat.app.AppCompatActivity;
7-
import androidx.core.graphics.Insets;
8-
import androidx.core.view.ViewCompat;
9-
import androidx.core.view.WindowInsetsCompat;
14+
15+
import java.io.File;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
19+
import click.vpnclient.engine.VpnClientEngine;
1020

1121
public class MainActivity extends AppCompatActivity {
22+
private static final String TAG = "MainActivity";
23+
24+
private Button startButton;
25+
private Button stopButton;
26+
private TextView statusTextView;
27+
private VpnClientEngine engine;
28+
private boolean isVpnRunning = false;
29+
30+
private final ActivityResultLauncher<Intent> vpnPermissionLauncher = registerForActivityResult(
31+
new ActivityResultContracts.StartActivityForResult(),
32+
result -> {
33+
if (result.getResultCode() == RESULT_OK) {
34+
Log.d(TAG, "VPN permission granted");
35+
startVpnConnection();
36+
} else {
37+
Log.w(TAG, "VPN permission denied");
38+
Toast.makeText(this, "VPN permission is required to start the connection", Toast.LENGTH_LONG).show();
39+
updateUI(false);
40+
}
41+
}
42+
);
1243

1344
@Override
1445
protected void onCreate(Bundle savedInstanceState) {
1546
super.onCreate(savedInstanceState);
16-
EdgeToEdge.enable(this);
1747
setContentView(R.layout.activity_main);
18-
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
19-
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
20-
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
21-
return insets;
22-
});
48+
49+
startButton = findViewById(R.id.start_button);
50+
stopButton = findViewById(R.id.stop_button);
51+
statusTextView = findViewById(R.id.status_text);
52+
53+
engine = new VpnClientEngine(VpnClientEngine.DriverType.LibXray);
54+
55+
// Create a basic config file if it doesn't exist
56+
createConfigFileIfNeeded();
57+
58+
startButton.setOnClickListener(v -> requestVpnPermission());
59+
60+
stopButton.setOnClickListener(v -> stopVpnConnection());
61+
62+
// Initialize UI
63+
updateUI(false);
64+
}
65+
66+
private void requestVpnPermission() {
67+
Intent vpnIntent = VpnService.prepare(this);
68+
if (vpnIntent != null) {
69+
// Permission not granted, request it
70+
Log.d(TAG, "Requesting VPN permission");
71+
vpnPermissionLauncher.launch(vpnIntent);
72+
} else {
73+
// Permission already granted
74+
Log.d(TAG, "VPN permission already granted");
75+
startVpnConnection();
76+
}
77+
}
78+
79+
private void startVpnConnection() {
80+
Log.d(TAG, "Starting VPN connection");
81+
82+
try {
83+
// Start VPN engine
84+
String dataDir = getApplicationInfo().dataDir;
85+
String configFilePath = new File(dataDir, "config.json").getAbsolutePath();
86+
boolean engineStarted = engine.start(dataDir, configFilePath);
87+
88+
if (!engineStarted) {
89+
Log.e(TAG, "Failed to start VPN engine");
90+
Toast.makeText(this, "Failed to start VPN engine", Toast.LENGTH_SHORT).show();
91+
updateUI(false);
92+
return;
93+
}
94+
95+
// Start VPN service
96+
Intent serviceIntent = new Intent(this, VPNService.class);
97+
startService(serviceIntent);
98+
99+
isVpnRunning = true;
100+
updateUI(true);
101+
Toast.makeText(this, "VPN connection started", Toast.LENGTH_SHORT).show();
102+
103+
} catch (Exception e) {
104+
Log.e(TAG, "Error starting VPN connection", e);
105+
Toast.makeText(this, "Error starting VPN: " + e.getMessage(), Toast.LENGTH_LONG).show();
106+
updateUI(false);
107+
}
108+
}
109+
110+
private void stopVpnConnection() {
111+
Log.d(TAG, "Stopping VPN connection");
112+
113+
try {
114+
// Stop VPN service
115+
Intent serviceIntent = new Intent(this, VPNService.class);
116+
serviceIntent.setAction("STOP_VPN");
117+
startService(serviceIntent);
118+
119+
// Stop VPN engine
120+
engine.stop();
121+
122+
isVpnRunning = false;
123+
updateUI(false);
124+
Toast.makeText(this, "VPN connection stopped", Toast.LENGTH_SHORT).show();
125+
126+
} catch (Exception e) {
127+
Log.e(TAG, "Error stopping VPN connection", e);
128+
Toast.makeText(this, "Error stopping VPN: " + e.getMessage(), Toast.LENGTH_SHORT).show();
129+
}
130+
}
131+
132+
private void updateUI(boolean isRunning) {
133+
startButton.setEnabled(!isRunning);
134+
stopButton.setEnabled(isRunning);
135+
136+
if (isRunning) {
137+
statusTextView.setText(R.string.status_running);
138+
} else {
139+
statusTextView.setText(R.string.status_stopped);
140+
}
141+
}
142+
143+
private void createConfigFileIfNeeded() {
144+
String dataDir = getApplicationInfo().dataDir;
145+
File configFile = new File(dataDir, "config.json");
146+
147+
if (!configFile.exists()) {
148+
try {
149+
// Create a basic configuration
150+
String basicConfig = "{\n" +
151+
" \"log\": {\n" +
152+
" \"loglevel\": \"info\"\n" +
153+
" },\n" +
154+
" \"inbounds\": [\n" +
155+
" {\n" +
156+
" \"tag\": \"tun\",\n" +
157+
" \"type\": \"tun\",\n" +
158+
" \"interface_name\": \"tun0\",\n" +
159+
" \"inet4_address\": \"172.19.0.1/30\",\n" +
160+
" \"auto_route\": true,\n" +
161+
" \"strict_route\": false,\n" +
162+
" \"sniff\": true\n" +
163+
" }\n" +
164+
" ],\n" +
165+
" \"outbounds\": [\n" +
166+
" {\n" +
167+
" \"tag\": \"direct\",\n" +
168+
" \"type\": \"direct\"\n" +
169+
" }\n" +
170+
" ]\n" +
171+
"}";
172+
173+
FileOutputStream fos = new FileOutputStream(configFile);
174+
fos.write(basicConfig.getBytes());
175+
fos.close();
176+
177+
Log.d(TAG, "Created basic config file at: " + configFile.getAbsolutePath());
178+
} catch (IOException e) {
179+
Log.e(TAG, "Failed to create config file", e);
180+
}
181+
}
182+
}
183+
184+
@Override
185+
protected void onDestroy() {
186+
super.onDestroy();
187+
if (isVpnRunning) {
188+
stopVpnConnection();
189+
}
23190
}
24191
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package click.vpnclient.engine.example;
2+
3+
import android.content.Intent;
4+
import android.net.VpnService;
5+
import android.os.ParcelFileDescriptor;
6+
import android.util.Log;
7+
8+
import java.io.IOException;
9+
10+
public class VPNService extends VpnService {
11+
private static final String TAG = "VPNService";
12+
private ParcelFileDescriptor vpnInterface;
13+
private Thread vpnThread;
14+
private boolean isRunning = false;
15+
16+
@Override
17+
public int onStartCommand(Intent intent, int flags, int startId) {
18+
Log.d(TAG, "VPN Service starting");
19+
20+
if (intent != null && "STOP_VPN".equals(intent.getAction())) {
21+
stopVPN();
22+
return START_NOT_STICKY;
23+
}
24+
25+
startVPN();
26+
return START_STICKY;
27+
}
28+
29+
private void startVPN() {
30+
if (isRunning) {
31+
Log.d(TAG, "VPN already running");
32+
return;
33+
}
34+
35+
try {
36+
// Create VPN interface
37+
Builder builder = new Builder()
38+
.setSession("VPN Client Engine")
39+
.addAddress("10.0.0.2", 24)
40+
.addDnsServer("8.8.8.8")
41+
.addDnsServer("8.8.4.4")
42+
.addRoute("0.0.0.0", 0);
43+
44+
vpnInterface = builder.establish();
45+
46+
if (vpnInterface == null) {
47+
Log.e(TAG, "Failed to establish VPN interface");
48+
return;
49+
}
50+
51+
isRunning = true;
52+
Log.d(TAG, "VPN interface established");
53+
54+
// Start VPN thread
55+
vpnThread = new Thread(this::runVPN);
56+
vpnThread.start();
57+
58+
} catch (Exception e) {
59+
Log.e(TAG, "Error starting VPN", e);
60+
stopVPN();
61+
}
62+
}
63+
64+
private void runVPN() {
65+
Log.d(TAG, "VPN thread started");
66+
67+
try {
68+
// Keep the VPN service running
69+
while (isRunning && !Thread.currentThread().isInterrupted()) {
70+
synchronized (this) {
71+
wait(5000); // Wait for 5 seconds or until interrupted
72+
}
73+
}
74+
} catch (InterruptedException e) {
75+
Log.d(TAG, "VPN thread interrupted");
76+
} finally {
77+
Log.d(TAG, "VPN thread stopped");
78+
}
79+
}
80+
81+
private void stopVPN() {
82+
Log.d(TAG, "Stopping VPN");
83+
isRunning = false;
84+
85+
if (vpnThread != null) {
86+
vpnThread.interrupt();
87+
try {
88+
vpnThread.join(1000);
89+
} catch (InterruptedException e) {
90+
Log.w(TAG, "Interrupted while waiting for VPN thread to stop");
91+
}
92+
vpnThread = null;
93+
}
94+
95+
if (vpnInterface != null) {
96+
try {
97+
vpnInterface.close();
98+
} catch (IOException e) {
99+
Log.e(TAG, "Error closing VPN interface", e);
100+
}
101+
vpnInterface = null;
102+
}
103+
104+
stopSelf();
105+
}
106+
107+
@Override
108+
public void onDestroy() {
109+
super.onDestroy();
110+
stopVPN();
111+
Log.d(TAG, "VPN Service destroyed");
112+
}
113+
114+
@Override
115+
public void onRevoke() {
116+
super.onRevoke();
117+
Log.d(TAG, "VPN permission revoked");
118+
stopVPN();
119+
}
120+
}

0 commit comments

Comments
 (0)