Showcase HTTP proxy usage in ToyVPN.

Test: manual.
Bug: 76001058
Change-Id: I08efa2ba8379114b88aa4e10c6fa733670df6995
diff --git a/samples/ToyVpn/res/layout/form.xml b/samples/ToyVpn/res/layout/form.xml
index 0f62e17..00cd55b 100644
--- a/samples/ToyVpn/res/layout/form.xml
+++ b/samples/ToyVpn/res/layout/form.xml
@@ -31,6 +31,30 @@
         <TextView style="@style/item" android:text="@string/secret"/>
         <EditText style="@style/item" android:id="@+id/secret" android:password="true"/>
 
+        <TextView style="@style/item" android:text="@string/proxyhost"/>
+        <EditText style="@style/item" android:id="@+id/proxyhost"/>
+
+        <TextView style="@style/item" android:text="@string/proxyport"/>
+        <EditText style="@style/item" android:id="@+id/proxyport" android:inputType="number"/>
+
+        <TextView style="@style/item" android:text="@string/packages"/>
+        <RadioGroup
+            style="@style/item"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+            <RadioButton
+                android:id="@+id/allowed"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/allowed"/>
+            <RadioButton
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/disallowed"/>
+        </RadioGroup>
+        <EditText style="@style/item" android:id="@+id/packages"/>
+
         <Button style="@style/item" android:id="@+id/connect" android:text="@string/connect"/>
         <Button style="@style/item" android:id="@+id/disconnect" android:text="@string/disconnect"/>
 
diff --git a/samples/ToyVpn/res/values/strings.xml b/samples/ToyVpn/res/values/strings.xml
index d5e06ba..c084fa5 100644
--- a/samples/ToyVpn/res/values/strings.xml
+++ b/samples/ToyVpn/res/values/strings.xml
@@ -23,8 +23,20 @@
     <string name="secret">Shared Secret:</string>
     <string name="connect">Connect!</string>
     <string name="disconnect">Disconnect!</string>
+    <string name="proxyhost">HTTP proxy hostname</string>
+    <string name="proxyport">HTTP proxy port</string>
+
+    <string name="packages">Packages (comma separated):</string>
+    <string name="allowed">Allow</string>
+    <string name="disallowed">Disallow</string>
 
     <string name="connecting">ToyVPN is connecting...</string>
     <string name="connected">ToyVPN is connected!</string>
     <string name="disconnected">ToyVPN is disconnected!</string>
+    <string name="incomplete_proxy_settings">
+        Incomplete proxy settings. For HTTP proxy we require both hostname and port settings.
+    </string>
+    <string name="unknown_package_names">
+        Some of the specified package names do not correspond to any installed packages.
+    </string>
 </resources>
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
index c6a72e9..6a4c161 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java
@@ -21,7 +21,14 @@
 import android.content.SharedPreferences;
 import android.net.VpnService;
 import android.os.Bundle;
+import android.widget.RadioButton;
 import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 public class ToyVpnClient extends Activity {
     public interface Prefs {
@@ -29,6 +36,10 @@
         String SERVER_ADDRESS = "server.address";
         String SERVER_PORT = "server.port";
         String SHARED_SECRET = "shared.secret";
+        String PROXY_HOSTNAME = "proxyhost";
+        String PROXY_PORT = "proxyport";
+        String ALLOW = "allow";
+        String PACKAGES = "packages";
     }
 
     @Override
@@ -36,22 +47,64 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.form);
 
-        final TextView serverAddress = (TextView) findViewById(R.id.address);
-        final TextView serverPort = (TextView) findViewById(R.id.port);
-        final TextView sharedSecret = (TextView) findViewById(R.id.secret);
+        final TextView serverAddress = findViewById(R.id.address);
+        final TextView serverPort = findViewById(R.id.port);
+        final TextView sharedSecret = findViewById(R.id.secret);
+        final TextView proxyHost = findViewById(R.id.proxyhost);
+        final TextView proxyPort = findViewById(R.id.proxyport);
+
+        final RadioButton allowed = findViewById(R.id.allowed);
+        final TextView packages = findViewById(R.id.packages);
 
         final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, MODE_PRIVATE);
         serverAddress.setText(prefs.getString(Prefs.SERVER_ADDRESS, ""));
-        serverPort.setText(prefs.getString(Prefs.SERVER_PORT, ""));
+        int serverPortPrefValue = prefs.getInt(Prefs.SERVER_PORT, 0);
+        serverPort.setText(String.valueOf(serverPortPrefValue == 0 ? "" : serverPortPrefValue));
         sharedSecret.setText(prefs.getString(Prefs.SHARED_SECRET, ""));
+        proxyHost.setText(prefs.getString(Prefs.PROXY_HOSTNAME, ""));
+        int proxyPortPrefValue = prefs.getInt(Prefs.PROXY_PORT, 0);
+        proxyPort.setText(proxyPortPrefValue == 0 ? "" : String.valueOf(proxyPortPrefValue));
+
+        allowed.setChecked(prefs.getBoolean(Prefs.ALLOW, true));
+        packages.setText(String.join(", ", prefs.getStringSet(
+                Prefs.PACKAGES, Collections.emptySet())));
 
         findViewById(R.id.connect).setOnClickListener(v -> {
+            if (!checkProxyConfigs(proxyHost.getText().toString(),
+                    proxyPort.getText().toString())) {
+                return;
+            }
+
+            final Set<String> packageSet =
+                    Arrays.stream(packages.getText().toString().split(","))
+                            .map(String::trim)
+                            .filter(s -> !s.isEmpty())
+                            .collect(Collectors.toSet());
+            if (!checkPackages(packageSet)) {
+                return;
+            }
+
+            int serverPortNum;
+            try {
+                serverPortNum = Integer.parseInt(serverPort.getText().toString());
+            } catch (NumberFormatException e) {
+                serverPortNum = 0;
+            }
+            int proxyPortNum;
+            try {
+                proxyPortNum = Integer.parseInt(proxyPort.getText().toString());
+            } catch (NumberFormatException e) {
+                proxyPortNum = 0;
+            }
             prefs.edit()
                     .putString(Prefs.SERVER_ADDRESS, serverAddress.getText().toString())
-                    .putString(Prefs.SERVER_PORT, serverPort.getText().toString())
+                    .putInt(Prefs.SERVER_PORT, serverPortNum)
                     .putString(Prefs.SHARED_SECRET, sharedSecret.getText().toString())
+                    .putString(Prefs.PROXY_HOSTNAME, proxyHost.getText().toString())
+                    .putInt(Prefs.PROXY_PORT, proxyPortNum)
+                    .putBoolean(Prefs.ALLOW, allowed.isChecked())
+                    .putStringSet(Prefs.PACKAGES, packageSet)
                     .commit();
-
             Intent intent = VpnService.prepare(ToyVpnClient.this);
             if (intent != null) {
                 startActivityForResult(intent, 0);
@@ -64,6 +117,26 @@
         });
     }
 
+    private boolean checkProxyConfigs(String proxyHost, String proxyPort) {
+        final boolean hasIncompleteProxyConfigs = proxyHost.isEmpty() != proxyPort.isEmpty();
+        if (hasIncompleteProxyConfigs) {
+            Toast.makeText(this, R.string.incomplete_proxy_settings, Toast.LENGTH_SHORT).show();
+        }
+        return !hasIncompleteProxyConfigs;
+    }
+
+    private boolean checkPackages(Set<String> packageNames) {
+        final boolean hasCorrectPackageNames = packageNames.isEmpty() ||
+                getPackageManager().getInstalledPackages(0).stream()
+                        .map(pi -> pi.packageName)
+                        .collect(Collectors.toSet())
+                        .containsAll(packageNames);
+        if (!hasCorrectPackageNames) {
+            Toast.makeText(this, R.string.unknown_package_names, Toast.LENGTH_SHORT).show();
+        }
+        return hasCorrectPackageNames;
+    }
+
     @Override
     protected void onActivityResult(int request, int result, Intent data) {
         if (result == RESULT_OK) {
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
index 6644ecd..46897d9 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java
@@ -19,8 +19,11 @@
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import android.app.PendingIntent;
+import android.content.pm.PackageManager;
+import android.net.ProxyInfo;
 import android.net.VpnService;
 import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.FileInputStream;
@@ -31,6 +34,7 @@
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.nio.channels.DatagramChannel;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 public class ToyVpnConnection implements Runnable {
@@ -83,14 +87,34 @@
     private PendingIntent mConfigureIntent;
     private OnEstablishListener mOnEstablishListener;
 
+    // Proxy settings
+    private String mProxyHostName;
+    private int mProxyHostPort;
+
+    // Allowed/Disallowed packages for VPN usage
+    private final boolean mAllow;
+    private final Set<String> mPackages;
+
     public ToyVpnConnection(final VpnService service, final int connectionId,
-            final String serverName, final int serverPort, final byte[] sharedSecret) {
+            final String serverName, final int serverPort, final byte[] sharedSecret,
+            final String proxyHostName, final int proxyHostPort, boolean allow,
+            final Set<String> packages) {
         mService = service;
         mConnectionId = connectionId;
 
         mServerName = serverName;
         mServerPort= serverPort;
         mSharedSecret = sharedSecret;
+
+        if (!TextUtils.isEmpty(proxyHostName)) {
+            mProxyHostName = proxyHostName;
+        }
+        if (proxyHostPort > 0) {
+            // The port value is always an integer due to the configured inputType.
+            mProxyHostPort = proxyHostPort;
+        }
+        mAllow = allow;
+        mPackages = packages;
     }
 
     /**
@@ -117,7 +141,7 @@
 
             // We try to create the tunnel several times.
             // TODO: The better way is to work with ConnectivityManager, trying only when the
-            //       network is available.
+            // network is available.
             // Here we just use a counter to keep things simple.
             for (int attempt = 0; attempt < 10; ++attempt) {
                 // Reset the counter if we were connected.
@@ -309,11 +333,23 @@
 
         // Create a new interface using the builder and save the parameters.
         final ParcelFileDescriptor vpnInterface;
+        for (String packageName : mPackages) {
+            try {
+                if (mAllow) {
+                    builder.addAllowedApplication(packageName);
+                } else {
+                    builder.addDisallowedApplication(packageName);
+                }
+            } catch (PackageManager.NameNotFoundException e){
+                Log.w(getTag(), "Package not available: " + packageName, e);
+            }
+        }
+        builder.setSession(mServerName).setConfigureIntent(mConfigureIntent);
+        if (!TextUtils.isEmpty(mProxyHostName)) {
+            builder.setHttpProxy(ProxyInfo.buildDirectProxy(mProxyHostName, mProxyHostPort));
+        }
         synchronized (mService) {
-            vpnInterface = builder
-                    .setSession(mServerName)
-                    .setConfigureIntent(mConfigureIntent)
-                    .establish();
+            vpnInterface = builder.establish();
             if (mOnEstablishListener != null) {
                 mOnEstablishListener.onEstablish(vpnInterface);
             }
diff --git a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
index 5e42d9d..8b28f34 100644
--- a/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
+++ b/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnService.java
@@ -17,8 +17,9 @@
 package com.example.android.toyvpn;
 
 import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.app.Service;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.net.VpnService;
@@ -30,6 +31,8 @@
 import android.widget.Toast;
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -101,17 +104,15 @@
         final SharedPreferences prefs = getSharedPreferences(ToyVpnClient.Prefs.NAME, MODE_PRIVATE);
         final String server = prefs.getString(ToyVpnClient.Prefs.SERVER_ADDRESS, "");
         final byte[] secret = prefs.getString(ToyVpnClient.Prefs.SHARED_SECRET, "").getBytes();
-        final int port;
-        try {
-            port = Integer.parseInt(prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, ""));
-        } catch (NumberFormatException e) {
-            Log.e(TAG, "Bad port: " + prefs.getString(ToyVpnClient.Prefs.SERVER_PORT, null), e);
-            return;
-        }
-
-        // Kick off a connection.
+        final boolean allow = prefs.getBoolean(ToyVpnClient.Prefs.ALLOW, true);
+        final Set<String> packages =
+                prefs.getStringSet(ToyVpnClient.Prefs.PACKAGES, Collections.emptySet());
+        final int port = prefs.getInt(ToyVpnClient.Prefs.SERVER_PORT, 0);
+        final String proxyHost = prefs.getString(ToyVpnClient.Prefs.PROXY_HOSTNAME, "");
+        final int proxyPort = prefs.getInt(ToyVpnClient.Prefs.PROXY_PORT, 0);
         startConnection(new ToyVpnConnection(
-                this, mNextConnectionId.getAndIncrement(), server, port, secret));
+                this, mNextConnectionId.getAndIncrement(), server, port, secret,
+                proxyHost, proxyPort, allow, packages));
     }
 
     private void startConnection(final ToyVpnConnection connection) {
@@ -121,13 +122,11 @@
 
         // Handler to mark as connected once onEstablish is called.
         connection.setConfigureIntent(mConfigureIntent);
-        connection.setOnEstablishListener(new ToyVpnConnection.OnEstablishListener() {
-            public void onEstablish(ParcelFileDescriptor tunInterface) {
-                mHandler.sendEmptyMessage(R.string.connected);
+        connection.setOnEstablishListener(tunInterface -> {
+            mHandler.sendEmptyMessage(R.string.connected);
 
-                mConnectingThread.compareAndSet(thread, null);
-                setConnection(new Connection(thread, tunInterface));
-            }
+            mConnectingThread.compareAndSet(thread, null);
+            setConnection(new Connection(thread, tunInterface));
         });
         thread.start();
     }
@@ -159,7 +158,13 @@
     }
 
     private void updateForegroundNotification(final int message) {
-        startForeground(1, new Notification.Builder(this)
+        final String NOTIFICATION_CHANNEL_ID = "ToyVpn";
+        NotificationManager mNotificationManager = (NotificationManager) getSystemService(
+                NOTIFICATION_SERVICE);
+        mNotificationManager.createNotificationChannel(new NotificationChannel(
+                NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID,
+                NotificationManager.IMPORTANCE_DEFAULT));
+        startForeground(1, new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
                 .setSmallIcon(R.drawable.ic_vpn)
                 .setContentText(getString(message))
                 .setContentIntent(mConfigureIntent)