Client: cross-platform app (tested on OSX and Android)
diff --git a/core/client/android/app/build.gradle b/core/client/android/app/build.gradle
index 44fe4cf..9102497 100644
--- a/core/client/android/app/build.gradle
+++ b/core/client/android/app/build.gradle
@@ -36,6 +36,7 @@
     implementation "androidx.activity:activity:1.3.1"
 
     implementation 'com.google.code.gson:gson:2.8.9'
+    implementation ':pcloud@aar'
 
     // Desugaring and multidex is required for API < 21.
     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
diff --git a/core/client/android/app/src/main/AndroidManifest.xml b/core/client/android/app/src/main/AndroidManifest.xml
index 24728da..a75444d 100644
--- a/core/client/android/app/src/main/AndroidManifest.xml
+++ b/core/client/android/app/src/main/AndroidManifest.xml
@@ -4,9 +4,10 @@
 
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.INTERNET" />
-    <!--uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /-->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
     <application
+        android:name=".PCloudApp"
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
@@ -15,14 +16,20 @@
         android:theme="@style/Theme.Pcloud"
         android:hardwareAccelerated="true">
         <activity
-            android:name=".MainActivity"
+            android:name=".PCloudActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <service android:name=".PCloudVPNService"
+            android:permission="android.permission.BIND_VPN_SERVICE"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.net.VpnService"/>
+            </intent-filter>
+        </service>
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/Base64TypeAdapter.java b/core/client/android/app/src/main/java/me/lekva/pcloud/Base64TypeAdapter.java
deleted file mode 100644
index eda2c58..0000000
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/Base64TypeAdapter.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package me.lekva.pcloud;
-
-import android.os.Build;
-import android.util.Base64;
-
-import androidx.annotation.RequiresApi;
-
-import com.google.gson.TypeAdapter;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-
-import java.io.IOException;
-
-public class Base64TypeAdapter extends TypeAdapter<byte[]> {
-    @RequiresApi(api = Build.VERSION_CODES.O)
-    @Override
-    public void write(JsonWriter out, byte[] value) throws IOException {
-        String val = Base64.encodeToString(value, Base64.NO_WRAP);
-        // out.value(val.substring(0, val.length() - 1));
-        out.value(val);
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.O)
-    @Override
-    public byte[] read(JsonReader in) throws IOException {
-        return Base64.decode(in.nextString(), Base64.NO_WRAP);
-    }
-}
\ No newline at end of file
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/MainActivity.java b/core/client/android/app/src/main/java/me/lekva/pcloud/MainActivity.java
deleted file mode 100644
index 4b62246..0000000
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/MainActivity.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package me.lekva.pcloud;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.journeyapps.barcodescanner.ScanContract;
-import com.journeyapps.barcodescanner.ScanOptions;
-
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-
-public class MainActivity extends AppCompatActivity {
-    private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),
-            result -> {
-                if(result.getContents() != null) {
-                    Gson gson = new GsonBuilder().disableHtmlEscaping().create();
-                    VPNApiServerConfig config = gson.fromJson(result.getContents(), VPNApiServerConfig.class);
-                    join(config);
-                }
-            });
-
-    private void join(VPNApiServerConfig config) {
-        new Thread(() -> {
-            VerifyRequest req = new VerifyRequest();
-            req.message = config.message;
-            req.signature = config.signature;
-            Gson gson = new GsonBuilder().disableHtmlEscaping().create();
-            byte[] data = gson.toJson(req).getBytes(StandardCharsets.UTF_8);
-            HttpURLConnection urlConnection = null;
-            try {
-                URL url = new URL(config.address + "/api/verify");
-                urlConnection = (HttpURLConnection) url.openConnection();
-                urlConnection.setRequestMethod("POST");
-                urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
-                urlConnection.setRequestProperty("Connection", "close");
-
-                urlConnection.setInstanceFollowRedirects(false);
-                urlConnection.setDoOutput(true);
-                urlConnection.setFixedLengthStreamingMode(data.length);
-
-                OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
-                out.write(data);
-                out.flush();
-                out.close();
-                System.out.println(urlConnection.getResponseCode());
-            } catch (IOException e) {
-                e.printStackTrace();
-            } finally {
-                urlConnection.disconnect();
-            }
-        }).start();
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-        Activity act = this;
-        findViewById(R.id.scan_qr_code).setOnClickListener(new View.OnClickListener() {
-            @RequiresApi(api = Build.VERSION_CODES.M)
-            @Override
-            public void onClick(View view) {
-                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
-                    System.out.println("fooooooo");
-                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.INTERNET}, 20);
-                }
-
-                ScanOptions options = new ScanOptions();
-                options.setDesiredBarcodeFormats(ScanOptions.QR_CODE);
-                options.setPrompt("Join PCloud network");
-                options.setCameraId(0);  // Use a specific camera of the device
-                options.setBeepEnabled(false);
-                options.setBarcodeImageEnabled(false);
-                barcodeLauncher.launch(options);
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java
new file mode 100644
index 0000000..3e737d4
--- /dev/null
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudActivity.java
@@ -0,0 +1,82 @@
+package me.lekva.pcloud;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.journeyapps.barcodescanner.ScanContract;
+import com.journeyapps.barcodescanner.ScanOptions;
+
+import org.gioui.GioView;
+
+public class PCloudActivity extends AppCompatActivity {
+    private GioView view;
+
+    private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),
+            result -> {
+                if(result.getContents() != null) {
+                    qrcodeScanned(result.getContents());
+                }
+            });
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        view = new GioView(this);
+        setContentView(view);
+        qrcodeScanned("yayaya");
+    }
+
+    @Override
+    public void onDestroy() {
+        view.destroy();
+        super.onDestroy();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        view.start();
+    }
+
+    @Override
+    public void onStop() {
+        view.stop();
+        super.onStop();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration c) {
+        super.onConfigurationChanged(c);
+        view.configurationChanged();
+    }
+
+    @Override
+    public void onLowMemory() {
+        super.onLowMemory();
+        view.onLowMemory();
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (!view.backPressed())
+            super.onBackPressed();
+    }
+
+    // TODO(giolekva): return void instead of String
+    public String launchBarcodeScanner() {
+        ScanOptions options = new ScanOptions();
+        options.setDesiredBarcodeFormats(ScanOptions.QR_CODE);
+        options.setPrompt("Join PCloud network");
+        options.setCameraId(0);  // Use a specific camera of the device
+        options.setBeepEnabled(true);
+        options.setBarcodeImageEnabled(false);
+        barcodeLauncher.launch(options);
+        return null;
+    }
+
+    private native void qrcodeScanned(String contents);
+}
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java
new file mode 100644
index 0000000..d8b5723
--- /dev/null
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudApp.java
@@ -0,0 +1,15 @@
+package me.lekva.pcloud;
+
+import android.app.Application;
+
+import org.gioui.Gio;
+
+public class PCloudApp extends Application {
+    @Override
+    public void onCreate() {
+        System.out.println("fooo");
+        super.onCreate();
+        Gio.init(this);
+        System.out.println("bar");
+    }
+}
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java
new file mode 100644
index 0000000..43ce99f
--- /dev/null
+++ b/core/client/android/app/src/main/java/me/lekva/pcloud/PCloudVPNService.java
@@ -0,0 +1,55 @@
+package me.lekva.pcloud;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.VpnService;
+import android.os.Handler;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+
+public class PCloudVPNService extends VpnService implements Handler.Callback {
+    public static final String ACTION_CONNECT = "CONNECT";
+    public static final String ACTION_DISCONNECT = "DISCONNECT";
+
+    private boolean running = false;
+    private Handler handler = null;
+
+    @Override
+    public void onCreate() {
+        if (handler == null) {
+            handler = new Handler(this);
+        }
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (intent != null && intent.getAction().equals(ACTION_DISCONNECT)) {
+            stopVpn();
+            return Service.START_NOT_STICKY;
+        } else {
+            startVpn();
+            return Service.START_STICKY;
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        stopVpn();
+    }
+
+    private void startVpn() {
+        System.out.println("--- START");
+    }
+
+    private void stopVpn() {
+        System.out.println("--- STOP");
+        running = false;
+    }
+
+    @Override
+    public boolean handleMessage(@NonNull Message message) {
+        System.out.println(getString(message.what));
+        return true;
+    }
+}
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/VPNApiServerConfig.java b/core/client/android/app/src/main/java/me/lekva/pcloud/VPNApiServerConfig.java
deleted file mode 100644
index 0128a76..0000000
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/VPNApiServerConfig.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package me.lekva.pcloud;
-
-import com.google.gson.annotations.JsonAdapter;
-import com.google.gson.annotations.SerializedName;
-
-public class VPNApiServerConfig {
-    @SerializedName("vpn_api_addr")
-    public String address;
-    @JsonAdapter(Base64TypeAdapter.class)
-    public byte[] message;
-    @JsonAdapter(Base64TypeAdapter.class)
-    public byte[] signature;
-}
diff --git a/core/client/android/app/src/main/java/me/lekva/pcloud/VerifyRequest.java b/core/client/android/app/src/main/java/me/lekva/pcloud/VerifyRequest.java
deleted file mode 100644
index 7feaef1..0000000
--- a/core/client/android/app/src/main/java/me/lekva/pcloud/VerifyRequest.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package me.lekva.pcloud;
-
-import com.google.gson.annotations.JsonAdapter;
-
-public class VerifyRequest {
-    @JsonAdapter(Base64TypeAdapter.class)
-    public byte[] message;
-    @JsonAdapter(Base64TypeAdapter.class)
-    public byte[] signature;
-}
diff --git a/core/client/android/settings.gradle b/core/client/android/settings.gradle
index f75bfc8..c030043 100644
--- a/core/client/android/settings.gradle
+++ b/core/client/android/settings.gradle
@@ -4,6 +4,9 @@
         google()
         mavenCentral()
         jcenter() // Warning: this repository is going to shut down soon
+        flatDir {
+            dirs 'app/libs'
+        }
     }
 }
 rootProject.name = "pcloud"