From 18ccd98389bbe25dedb7b2e78b4b1d37f6ed928e Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Mon, 5 Mar 2018 00:20:46 +0100 Subject: Implement support for setting proxies --- .../main/java/de/blinkt/openvpn/VpnProfile.java | 9 +- .../java/de/blinkt/openvpn/core/Connection.java | 10 + .../de/blinkt/openvpn/core/OpenVPNService.java | 101 +++--- .../openvpn/core/OpenVpnManagementThread.java | 90 ++++- .../openvpn/fragments/ConnectionsAdapter.java | 366 ++++++++++++--------- main/src/main/res/layout/server_card.xml | 117 ++++++- main/src/main/res/values/strings.xml | 5 +- 7 files changed, 486 insertions(+), 212 deletions(-) (limited to 'main') diff --git a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java index 76bb502e..3ee293dc 100644 --- a/main/src/main/java/de/blinkt/openvpn/VpnProfile.java +++ b/main/src/main/java/de/blinkt/openvpn/VpnProfile.java @@ -61,7 +61,7 @@ public class VpnProfile implements Serializable, Cloneable { private static final long serialVersionUID = 7085688938959334563L; public static final int MAXLOGLEVEL = 4; - public static final int CURRENT_PROFILE_VERSION = 6; + public static final int CURRENT_PROFILE_VERSION = 7; public static final int DEFAULT_MSSFIX_SIZE = 1280; public static String DEFAULT_DNS1 = "8.8.8.8"; public static String DEFAULT_DNS2 = "8.8.4.4"; @@ -244,6 +244,7 @@ public class VpnProfile implements Serializable, Cloneable { } if (mAllowedAppsVpn == null) mAllowedAppsVpn = new HashSet<>(); + if (mConnections == null) mConnections = new Connection[0]; @@ -251,7 +252,11 @@ public class VpnProfile implements Serializable, Cloneable { if (TextUtils.isEmpty(mProfileCreator)) mUserEditable = true; } - + if (mProfileVersion < 7) { + for (Connection c: mConnections) + if (c.mProxyType == null) + c.mProxyType = Connection.ProxyType.NONE; + } mProfileVersion = CURRENT_PROFILE_VERSION; diff --git a/main/src/main/java/de/blinkt/openvpn/core/Connection.java b/main/src/main/java/de/blinkt/openvpn/core/Connection.java index ff15daec..748455ec 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/Connection.java +++ b/main/src/main/java/de/blinkt/openvpn/core/Connection.java @@ -19,6 +19,16 @@ public class Connection implements Serializable, Cloneable { public boolean mEnabled = true; public int mConnectTimeout = 0; public static final int CONNECTION_DEFAULT_TIMEOUT = 120; + public ProxyType mProxyType = ProxyType.NONE; + public String mProxyName = "proxy.example.com"; + public String mProxyPort = "8080"; + + public enum ProxyType { + NONE, + HTTP, + SOCKS5, + ORBOT + } private static final long serialVersionUID = 92031902903829089L; diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java index c79a0b99..fd11a911 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java @@ -15,7 +15,6 @@ import android.app.UiModeManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.Configuration; @@ -47,7 +46,6 @@ import java.util.Collection; import java.util.Locale; import java.util.Vector; -import de.blinkt.openvpn.BuildConfig; import de.blinkt.openvpn.LaunchVPN; import de.blinkt.openvpn.R; import de.blinkt.openvpn.VpnProfile; @@ -58,8 +56,6 @@ import de.blinkt.openvpn.core.VpnStatus.ByteCountListener; import de.blinkt.openvpn.core.VpnStatus.StateListener; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_START; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT; import static de.blinkt.openvpn.core.NetworkSpace.ipAddress; @@ -68,17 +64,22 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY"; public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE"; public static final String DISCONNECT_VPN = "de.blinkt.openvpn.DISCONNECT_VPN"; - private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN"; - private static final String RESUME_VPN = "de.blinkt.openvpn.RESUME_VPN"; public static final String NOTIFICATION_CHANNEL_BG_ID = "openvpn_bg"; public static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "openvpn_newstat"; public static final String VPNSERVICE_TUN = "vpnservice-tun"; - private String lastChannel; - + public final static String ORBOT_PACKAGE_NAME = "org.torproject.android"; + private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN"; + private static final String RESUME_VPN = "de.blinkt.openvpn.RESUME_VPN"; + private static final int PRIORITY_MIN = -2; + private static final int PRIORITY_DEFAULT = 0; + private static final int PRIORITY_MAX = 2; private static boolean mNotificationAlwaysVisible = false; + private static Class mNotificationActivityClass; private final Vector mDnslist = new Vector<>(); private final NetworkSpace mRoutes = new NetworkSpace(); private final NetworkSpace mRoutesv6 = new NetworkSpace(); + private final Object mProcessLock = new Object(); + private String lastChannel; private Thread mProcessThread = null; private VpnProfile mProfile; private String mDomain = null; @@ -90,19 +91,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac private boolean mStarting = false; private long mConnecttime; private OpenVPNManagement mManagement; - private String mLastTunCfg; - private String mRemoteGW; - private final Object mProcessLock = new Object(); - private Handler guiHandler; - private Toast mlastToast; - private Runnable mOpenVPNThread; - private static Class mNotificationActivityClass; - - private static final int PRIORITY_MIN = -2; - private static final int PRIORITY_DEFAULT = 0; - private static final int PRIORITY_MAX = 2; - - private final IBinder mBinder = new IOpenVPNServiceInternal.Stub() { @Override @@ -127,12 +115,11 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac }; - - @Override - public void addAllowedExternalApp(String packagename) throws RemoteException { - ExternalAppDatabase extapps = new ExternalAppDatabase(OpenVPNService.this); - extapps.addApp(packagename); - } + private String mLastTunCfg; + private String mRemoteGW; + private Handler guiHandler; + private Toast mlastToast; + private Runnable mOpenVPNThread; // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java public static String humanReadableByteCount(long bytes, boolean speed, Resources res) { @@ -172,6 +159,21 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } + /** + * Sets the activity which should be opened when tapped on the permanent notification tile. + * + * @param activityClass The activity class to open + */ + public static void setNotificationActivityClass(Class activityClass) { + mNotificationActivityClass = activityClass; + } + + @Override + public void addAllowedExternalApp(String packagename) throws RemoteException { + ExternalAppDatabase extapps = new ExternalAppDatabase(OpenVPNService.this); + extapps.addApp(packagename); + } + @Override public IBinder onBind(Intent intent) { String action = intent.getAction(); @@ -211,7 +213,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } } - private void showNotification(final String msg, String tickerText, @NonNull String channel, long when, ConnectionStatus status) { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); int icon = getIconByConnectionStatus(status); @@ -255,8 +256,8 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac //noinspection NewApi nbuilder.setChannelId(channel); if (mProfile != null) - //noinspection NewApi - nbuilder.setShortcutId(mProfile.getUUIDString()); + //noinspection NewApi + nbuilder.setShortcutId(mProfile.getUUIDString()); } @@ -370,15 +371,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } - /** - * Sets the activity which should be opened when tapped on the permanent notification tile. - * - * @param activityClass The activity class to open - */ - public static void setNotificationActivityClass(Class activityClass) { - mNotificationActivityClass = activityClass; - } - PendingIntent getUserInputIntent(String needed) { Intent intent = new Intent(getApplicationContext(), LaunchVPN.class); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); @@ -589,8 +581,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac } Runnable processThread; - if (useOpenVPN3) - { + if (useOpenVPN3) { OpenVPNManagement mOpenVPN3 = instantiateOpenVPN3Core(); processThread = (Runnable) mOpenVPN3; mManagement = mOpenVPN3; @@ -905,14 +896,36 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setAllowedVpnPackages(Builder builder) { + boolean profileUsesOrBot = false; + + for (Connection c : mProfile.mConnections) { + if (c.mProxyType == Connection.ProxyType.ORBOT) + profileUsesOrBot = true; + } + + if (profileUsesOrBot) + VpnStatus.logDebug("VPN Profile uses at least one server entry with Orbot. Setting up VPN so that OrBot is not redirected over VPN."); + + boolean atLeastOneAllowedApp = false; + + if (mProfile.mAllowedAppsVpnAreDisallowed && profileUsesOrBot) { + try { + builder.addDisallowedApplication(ORBOT_PACKAGE_NAME); + } catch (PackageManager.NameNotFoundException e) { + VpnStatus.logDebug("Orbot not installed?"); + } + } + for (String pkg : mProfile.mAllowedAppsVpn) { try { if (mProfile.mAllowedAppsVpnAreDisallowed) { builder.addDisallowedApplication(pkg); } else { - builder.addAllowedApplication(pkg); - atLeastOneAllowedApp = true; + if (!(profileUsesOrBot && pkg.equals(ORBOT_PACKAGE_NAME))) { + builder.addAllowedApplication(pkg); + atLeastOneAllowedApp = true; + } } } catch (PackageManager.NameNotFoundException e) { mProfile.mAllowedAppsVpn.remove(pkg); @@ -1051,7 +1064,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac if (mLocalIP.len <= 31 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CIDRIP interfaceRoute = new CIDRIP(mLocalIP.mIp, mLocalIP.len); interfaceRoute.normalise(); - addRoute(interfaceRoute ,true); + addRoute(interfaceRoute, true); } diff --git a/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java index 096982f9..5285b42f 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java +++ b/main/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java @@ -6,6 +6,7 @@ package de.blinkt.openvpn.core; import android.content.Context; +import android.content.Intent; import android.net.LocalServerSocket; import android.net.LocalSocket; import android.net.LocalSocketAddress; @@ -375,22 +376,94 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { private void processProxyCMD(String argument) { String[] args = argument.split(",", 3); - SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); + Connection.ProxyType proxyType = Connection.ProxyType.NONE; - if (args.length >= 2) { + int connectionEntryNumber = Integer.parseInt(args[0]) - 1; + String proxyport = null; + String proxyname = null; + + if (mProfile.mConnections.length > connectionEntryNumber) { + Connection connection = mProfile.mConnections[connectionEntryNumber]; + proxyType = connection.mProxyType; + proxyname = connection.mProxyName; + proxyport = connection.mProxyPort; + } else { + VpnStatus.logError(String.format(Locale.ENGLISH, "OpenVPN is asking for a proxy of an unknonwn connection entry (%d)", connectionEntryNumber)); + } + + // atuo detection of proxy + if (proxyType == Connection.ProxyType.NONE) { + SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile); + if (proxyaddr instanceof InetSocketAddress) { + InetSocketAddress isa = (InetSocketAddress) proxyaddr; + proxyType = Connection.ProxyType.HTTP; + proxyname = isa.getHostName(); + proxyport = String.valueOf(isa.getPort()); + } + } + + + if (args.length >= 2 && proxyType == Connection.ProxyType.HTTP) { String proto = args[1]; if (proto.equals("UDP")) { - proxyaddr = null; + proxyname = null; + VpnStatus.logInfo("Not using an HTTP proxy since the connection uses UDP"); } } - if (proxyaddr instanceof InetSocketAddress) { - InetSocketAddress isa = (InetSocketAddress) proxyaddr; - VpnStatus.logInfo(R.string.using_proxy, isa.getHostName(), isa.getPort()); + if (proxyType == Connection.ProxyType.ORBOT) { + // schwabe: TODO WIP and does not really work + /* OrbotHelper orbotHelper = OrbotHelper.get(mOpenVPNService); + orbotHelper.addStatusCallback(new StatusCallback() { + @Override + public void onEnabled(Intent statusIntent) { + VpnStatus.logDebug("Orbot onEnabled:" + statusIntent.toString()); + } + + @Override + public void onStarting() { + VpnStatus.logDebug("Orbot onStarting"); + } + + @Override + public void onStopping() { + VpnStatus.logDebug("Orbot onStopping"); + } - String proxycmd = String.format(Locale.ENGLISH, "proxy HTTP %s %d\n", isa.getHostName(), isa.getPort()); + @Override + public void onDisabled() { + VpnStatus.logDebug("Orbot onDisabled"); + } + + @Override + public void onStatusTimeout() { + VpnStatus.logDebug("Orbot onStatusTimeout"); + } + + @Override + public void onNotYetInstalled() { + VpnStatus.logDebug("Orbot notyetinstalled"); + } + }); + orbotHelper.init(); + if(!OrbotHelper.requestStartTor(mOpenVPNService)) + + VpnStatus.logError("Request starting Orbot failed."); + */ + proxyname = "127.0.0.1"; + proxyport = "8118"; + proxyType = Connection.ProxyType.HTTP; + + } + if (proxyType != Connection.ProxyType.NONE && proxyname != null) { + + VpnStatus.logInfo(R.string.using_proxy, proxyname, proxyport); + + String proxycmd = String.format(Locale.ENGLISH, "proxy %s %s %s\n", + proxyType == Connection.ProxyType.HTTP ? "HTTP" : "SOCKS", + proxyname, proxyport); managmentCommand(proxycmd); } else { managmentCommand("proxy NONE\n"); @@ -547,8 +620,9 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement { try { // Ignore Auth token message, already managed by openvpn itself - if (argument.startsWith("Auth-Token:")) + if (argument.startsWith("Auth-Token:")) { return; + } int p1 = argument.indexOf('\''); int p2 = argument.indexOf('\'', p1 + 1); diff --git a/main/src/main/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java b/main/src/main/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java index af071d37..bb930eaf 100644 --- a/main/src/main/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java +++ b/main/src/main/java/de/blinkt/openvpn/fragments/ConnectionsAdapter.java @@ -7,7 +7,6 @@ package de.blinkt.openvpn.fragments; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; @@ -15,7 +14,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; import android.widget.RadioGroup; @@ -29,14 +27,13 @@ import de.blinkt.openvpn.VpnProfile; import de.blinkt.openvpn.core.Connection; public class ConnectionsAdapter extends RecyclerView.Adapter { + private static final int TYPE_NORMAL = 0; + private static final int TYPE_FOOTER = TYPE_NORMAL + 1; private final Context mContext; private final VpnProfile mProfile; private final Settings_Connections mConnectionFragment; private Connection[] mConnections; - private static final int TYPE_NORMAL = 0; - private static final int TYPE_FOOTER = TYPE_NORMAL + 1; - ConnectionsAdapter(Context c, Settings_Connections connections_fragments, VpnProfile vpnProfile) { mContext = c; mConnections = vpnProfile.mConnections; @@ -44,6 +41,134 @@ public class ConnectionsAdapter extends RecyclerView.Adapter { + if (mConnection != null) { + mConnection.mEnabled = isChecked; + mConnectionsAdapter.displayWarningIfNoneEnabled(); } }); - mProtoGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(RadioGroup group, int checkedId) { - if (mConnection != null) { - if (checkedId == R.id.udp_proto) - mConnection.mUseUdp = true; - else if (checkedId == R.id.tcp_proto) - mConnection.mUseUdp = false; + mProtoGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (mConnection != null) { + if (checkedId == R.id.udp_proto) + mConnection.mUseUdp = true; + else if (checkedId == R.id.tcp_proto) + mConnection.mUseUdp = false; + } + }); + + mProxyGroup.setOnCheckedChangeListener((group, checkedId) -> { + if (mConnection != null) { + switch (checkedId) { + case R.id.proxy_none: + mConnection.mProxyType = Connection.ProxyType.NONE; + break; + case R.id.proxy_http: + mConnection.mProxyType = Connection.ProxyType.HTTP; + break; + case R.id.proxy_socks: + mConnection.mProxyType = Connection.ProxyType.SOCKS5; + break; + case R.id.proxy_orbot: + mConnection.mProxyType = Connection.ProxyType.ORBOT; + break; } + setVisibilityProxyServer(this, mConnection); } }); @@ -110,13 +259,10 @@ public class ConnectionsAdapter extends RecyclerView.Adapter { + if (mConnection != null) { + mConnection.mUseCustomConfig = isChecked; + mCustomOptionsLayout.setVisibility(mConnection.mUseCustomConfig ? View.VISIBLE : View.GONE); } }); @@ -140,6 +286,25 @@ public class ConnectionsAdapter extends RecyclerView.Adapter { + AlertDialog.Builder ab = new AlertDialog.Builder(mContext); + ab.setTitle(R.string.query_delete_remote); + ab.setPositiveButton(R.string.keep, null); + ab.setNegativeButton(R.string.delete, (dialog, which) -> { + removeRemote(getAdapterPosition()); + notifyItemRemoved(getAdapterPosition()); + }); + ab.create().show(); } ); } - - - } - - - @Override - public ConnectionsAdapter.ConnectionsHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - LayoutInflater li = LayoutInflater.from(mContext); - - View card; - if (viewType == TYPE_NORMAL) { - card = li.inflate(R.layout.server_card, viewGroup, false); - - } else { // TYPE_FOOTER - card = li.inflate(R.layout.server_footer, viewGroup, false); - } - return new ConnectionsHolder(card, this, viewType); - - } - - static abstract class OnTextChangedWatcher implements 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) { - - } - } - - @Override - public void onBindViewHolder(final ConnectionsAdapter.ConnectionsHolder cH, int position) { - if (position == mConnections.length) { - // Footer - return; - } - final Connection connection = mConnections[position]; - - cH.mConnection = null; - - cH.mPortNumberView.setText(connection.mServerPort); - cH.mServerNameView.setText(connection.mServerName); - cH.mPortNumberView.setText(connection.mServerPort); - cH.mRemoteSwitch.setChecked(connection.mEnabled); - - - cH.mConnectText.setText(String.valueOf(connection.getTimeout())); - - cH.mConnectSlider.setProgress(connection.getTimeout()); - - - cH.mProtoGroup.check(connection.mUseUdp ? R.id.udp_proto : R.id.tcp_proto); - - cH.mCustomOptionsLayout.setVisibility(connection.mUseCustomConfig ? View.VISIBLE : View.GONE); - cH.mCustomOptionText.setText(connection.mCustomConfiguration); - - cH.mCustomOptionCB.setChecked(connection.mUseCustomConfig); - cH.mConnection = connection; - - } - - - private void removeRemote(int idx) { - Connection[] mConnections2 = Arrays.copyOf(mConnections, mConnections.length - 1); - for (int i = idx + 1; i < mConnections.length; i++) { - mConnections2[i - 1] = mConnections[i]; - } - mConnections = mConnections2; - - } - - @Override - public int getItemCount() { - return mConnections.length + 1; //for footer - } - - @Override - public int getItemViewType(int position) { - if (position == mConnections.length) - return TYPE_FOOTER; - else - return TYPE_NORMAL; - } - - void addRemote() { - mConnections = Arrays.copyOf(mConnections, mConnections.length + 1); - mConnections[mConnections.length - 1] = new Connection(); - notifyItemInserted(mConnections.length - 1); - displayWarningIfNoneEnabled(); - } - - void displayWarningIfNoneEnabled() { - int showWarning = View.VISIBLE; - for (Connection conn : mConnections) { - if (conn.mEnabled) - showWarning = View.GONE; - } - mConnectionFragment.setWarningVisible(showWarning); - } - - - void saveProfile() { - mProfile.mConnections = mConnections; } } diff --git a/main/src/main/res/layout/server_card.xml b/main/src/main/res/layout/server_card.xml index e24a8fd7..db802468 100644 --- a/main/src/main/res/layout/server_card.xml +++ b/main/src/main/res/layout/server_card.xml @@ -154,11 +154,126 @@ android:text="Socks" /> --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 2c26580a..25adb5a1 100755 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -199,7 +199,7 @@ Load tun module Import PKCS12 from configuration into Android Keystore Error getting proxy settings: %s - Using proxy %1$s %2$d + Using proxy %1$s %2$s Use system proxy Use the system wide configuration for HTTP/HTTPS proxies to connect. OpenVPN will connect the specified VPN if it was active on system boot. Please read the connection warning FAQ before using this option on Android < 5.0. @@ -465,5 +465,8 @@ An external app tries to control %s. The app requesting access cannot be determined. Allowing this app grants ALL apps access. The OpenVPN 3 C++ implementation does not support static keys. Please change to OpenVPN 2.x under general settings. Using PKCS12 files directly with OpenVPN 3 C++ implementation is not supported. Please import the pkcs12 files into the Android keystore or change to OpenVPN 2.x under general settings. + Proxy + None + Tor (Orbot) -- cgit v1.2.3