Add proxy settings

This commit is contained in:
Martin Fietz 2016-02-27 23:34:45 +01:00
parent bb45d82b08
commit 19e1e4afdb
8 changed files with 542 additions and 5 deletions

View File

@ -0,0 +1,323 @@
package de.danoeh.antennapod.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v4.content.ContextCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.internal.MDButton;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class ProxyDialog {
private static final String TAG = "ProxyDialog";
private Context context;
private MaterialDialog dialog;
private Spinner spType;
private EditText etHost;
private EditText etPort;
private EditText etUsername;
private EditText etPassword;
private boolean testSuccessful = false;
private TextView txtvMessage;
private Subscription subscription;
public ProxyDialog(Context context) {
this.context = context;
}
public Dialog createDialog() {
dialog = new MaterialDialog.Builder(context)
.title(R.string.pref_proxy_title)
.customView(R.layout.proxy_settings, true)
.positiveText(R.string.proxy_test_label)
.negativeText(R.string.cancel_label)
.onPositive((dialog1, which) -> {
if(!testSuccessful) {
dialog.getActionButton(DialogAction.POSITIVE).setEnabled(false);
test();
return;
}
String type = (String) ((Spinner) dialog1.findViewById(R.id.spType)).getSelectedItem();
ProxyConfig proxy;
if(Proxy.Type.valueOf(type) == Proxy.Type.DIRECT) {
proxy = ProxyConfig.direct();
} else {
String host = etHost.getText().toString();
String port = etPort.getText().toString();
String username = etUsername.getText().toString();
if(TextUtils.isEmpty(username)) {
username = null;
}
String password = etPassword.getText().toString();
if(TextUtils.isEmpty(password)) {
password = null;
}
int portValue = 0;
if(!TextUtils.isEmpty(port)) {
portValue = Integer.valueOf(port);
}
proxy = ProxyConfig.http(host, portValue, username, password);
}
UserPreferences.setProxyConfig(proxy);
AntennapodHttpClient.reinit();
dialog.dismiss();
})
.onNegative((dialog1, which) -> {
dialog1.dismiss();
})
.autoDismiss(false)
.build();
View view = dialog.getCustomView();
spType = (Spinner) view.findViewById(R.id.spType);
String[] types = { Proxy.Type.DIRECT.name(), Proxy.Type.HTTP.name() };
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
android.R.layout.simple_spinner_item, types);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spType.setAdapter(adapter);
ProxyConfig proxyConfig = UserPreferences.getProxyConfig();
spType.setSelection(adapter.getPosition(proxyConfig.type.name()));
etHost = (EditText) view.findViewById(R.id.etHost);
if(!TextUtils.isEmpty(proxyConfig.host)) {
etHost.setText(proxyConfig.host);
}
etHost.addTextChangedListener(requireTestOnChange);
etPort = (EditText) view.findViewById(R.id.etPort);
if(proxyConfig.port > 0) {
etPort.setText(String.valueOf(proxyConfig.port));
}
etPort.addTextChangedListener(requireTestOnChange);
etUsername = (EditText) view.findViewById(R.id.etUsername);
if(!TextUtils.isEmpty(proxyConfig.username)) {
etUsername.setText(proxyConfig.username);
}
etUsername.addTextChangedListener(requireTestOnChange);
etPassword = (EditText) view.findViewById(R.id.etPassword);
if(!TextUtils.isEmpty(proxyConfig.password)) {
etPassword.setText(proxyConfig.username);
}
etPassword.addTextChangedListener(requireTestOnChange);
if(proxyConfig.type == Proxy.Type.DIRECT) {
enableSettings(false);
setTestRequired(false);
}
spType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
enableSettings(position > 0);
setTestRequired(position > 0);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
enableSettings(false);
}
});
txtvMessage = (TextView) view.findViewById(R.id.txtvMessage);
checkValidity();
return dialog;
}
private final TextWatcher requireTestOnChange = 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) {}
@Override
public void afterTextChanged(Editable s) {
setTestRequired(true);
}
};
private void enableSettings(boolean enable) {
etHost.setEnabled(enable);
etPort.setEnabled(enable);
etUsername.setEnabled(enable);
etPassword.setEnabled(enable);
}
private boolean checkValidity() {
boolean valid = true;
if(spType.getSelectedItemPosition() > 0) {
valid &= checkHost();
}
valid &= checkPort();
return valid;
}
private boolean checkHost() {
String host = etHost.getText().toString();
if(host.length() == 0) {
etHost.setError(context.getString(R.string.proxy_host_empty_error));
return false;
}
if(!"localhost".equals(host) && !Patterns.DOMAIN_NAME.matcher(host).matches()) {
etHost.setError(context.getString(R.string.proxy_host_invalid_error));
return false;
}
return true;
}
private boolean checkPort() {
int port = getPort();
if(port < 0 && port > 65535) {
etPort.setError(context.getString(R.string.proxy_port_invalid_error));
return false;
}
return true;
}
private int getPort() {
String port = etPort.getText().toString();
if(port.length() > 0) {
try {
int portValue = Integer.parseInt(port);
return portValue;
} catch(NumberFormatException e) {
// ignore
}
}
return 0;
}
private void setTestRequired(boolean required) {
if(required) {
testSuccessful = false;
MDButton button = dialog.getActionButton(DialogAction.POSITIVE);
button.setText(context.getText(R.string.proxy_test_label));
button.setEnabled(true);
} else {
testSuccessful = true;
MDButton button = dialog.getActionButton(DialogAction.POSITIVE);
button.setText(context.getText(android.R.string.ok));
button.setEnabled(true);
}
}
private void test() {
if(subscription != null) {
subscription.unsubscribe();
}
if(!checkValidity()) {
setTestRequired(true);
return;
}
TypedArray res = context.getTheme().obtainStyledAttributes(new int[] { android.R.attr.textColorPrimary });
int textColorPrimary = res.getColor(0, 0);
res.recycle();
String checking = context.getString(R.string.proxy_checking);
txtvMessage.setTextColor(textColorPrimary);
txtvMessage.setText("{fa-circle-o-notch spin} " + checking);
txtvMessage.setVisibility(View.VISIBLE);
subscription = Observable.create(new Observable.OnSubscribe<Response>() {
@Override
public void call(Subscriber<? super Response> subscriber) {
String type = (String) spType.getSelectedItem();
String host = etHost.getText().toString();
String port = etPort.getText().toString();
String username = etUsername.getText().toString();
String password = etPassword.getText().toString();
int portValue = 8080;
if(!TextUtils.isEmpty(port)) {
portValue = Integer.valueOf(port);
}
SocketAddress address = InetSocketAddress.createUnresolved(host, portValue);
Proxy.Type proxyType = Proxy.Type.valueOf(type.toUpperCase());
Proxy proxy = new Proxy(proxyType, address);
OkHttpClient client = AntennapodHttpClient.newHttpClient();
client.setConnectTimeout(10, TimeUnit.SECONDS);
client.setProxy(proxy);
client.interceptors().clear();
if(!TextUtils.isEmpty(username)) {
String credentials = Credentials.basic(username, password);
client.interceptors().add(chain -> {
Request request = chain.request().newBuilder()
.header("Proxy-Authorization", credentials).build();
return chain.proceed(request);
});
}
Request request = new Request.Builder()
.url("http://www.google.com")
.head()
.build();
try {
Response response = client.newCall(request).execute();
subscriber.onNext(response);
} catch(IOException e) {
subscriber.onError(e);
}
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
int colorId;
String icon;
String result;
if(response.isSuccessful()) {
colorId = R.color.download_success_green;
icon = "{fa-check}";
result = context.getString(R.string.proxy_test_successful);
} else {
colorId = R.color.download_failed_red;
icon = "{fa-close}";
result = context.getString(R.string.proxy_test_failed);
}
int color = ContextCompat.getColor(context, colorId);
txtvMessage.setTextColor(color);
String message = String.format("%s %s: %s", icon, result, response.message());
txtvMessage.setText(message);
setTestRequired(!response.isSuccessful());
},
error -> {
String icon = "{fa-close}";
String result = context.getString(R.string.proxy_test_failed);
int color = ContextCompat.getColor(context, R.color.download_failed_red);
txtvMessage.setTextColor(color);
String message = String.format("%s %s: %s", icon, result, error.getMessage());
txtvMessage.setText(message);
setTestRequired(true);
}
);
}
}

View File

@ -58,6 +58,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog;
import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog;
import de.danoeh.antennapod.dialog.ProxyDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
/**
@ -82,6 +83,7 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
public static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
public static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname";
public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
public static final String PREF_PROXY = "prefProxy";
private final PreferenceUI ui;
@ -356,6 +358,11 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
return false;
}
);
ui.findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> {
ProxyDialog dialog = new ProxyDialog(ui.getActivity());
dialog.createDialog().show();
return true;
});
ui.findPreference("prefSendCrashReport").setOnPreferenceClickListener(preference -> {
Intent emailIntent = new Intent(Intent.ACTION_SEND);
emailIntent.setType("text/plain");

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/txtvType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/proxy_type_label"
android:textColor="?android:attr/textColorSecondary" />
<Spinner
android:id="@+id/spType"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtvHost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/host_label"
android:textColor="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/etHost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textUri"
android:hint="www.example.com" />
<TextView
android:id="@+id/txtvPort"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/port_label"
android:textColor="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/etPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="8080"
android:inputType="number" />
<TextView
android:id="@+id/txtvUsername"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:singleLine="true"
android:text="@string/username_label"
android:textColor="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional_hint" />
<TextView
android:id="@+id/txtvPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/password_label"
android:textColor="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/optional_hint"
android:inputType="textPassword" />
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/txtvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="invisible"
android:gravity="center"/>
</LinearLayout>

View File

@ -200,8 +200,11 @@
android:key="prefEnableAutoDownloadWifiFilter"
android:title="@string/pref_autodl_wifi_filter_title"
android:summary="@string/pref_autodl_wifi_filter_sum"/>
</PreferenceScreen>
<Preference
android:key="prefProxy"
android:summary="@string/pref_proxy_sum"
android:title="@string/pref_proxy_title" />
</PreferenceCategory>

View File

@ -17,6 +17,7 @@ import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -25,6 +26,7 @@ import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
@ -78,6 +80,11 @@ public class UserPreferences {
public static final String PREF_ENABLE_AUTODL_ON_BATTERY = "prefEnableAutoDownloadOnBattery";
public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter";
public static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks";
public static final String PREF_PROXY_TYPE = "prefProxyType";
public static final String PREF_PROXY_HOST = "prefProxyHost";
public static final String PREF_PROXY_PORT = "prefProxyPort";
public static final String PREF_PROXY_USER = "prefProxyUser";
public static final String PREF_PROXY_PASSWORD = "prefProxyPassword";
// Services
public static final String PREF_AUTO_FLATTR = "pref_auto_flattr";
@ -371,6 +378,42 @@ public class UserPreferences {
return TextUtils.split(selectedNetWorks, ",");
}
public static void setProxyConfig(ProxyConfig config) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_PROXY_TYPE, config.type.name());
if(TextUtils.isEmpty(config.host)) {
editor.remove(PREF_PROXY_HOST);
} else {
editor.putString(PREF_PROXY_HOST, config.host);
}
if(config.port <= 0 || config.port > 65535) {
editor.remove(PREF_PROXY_PORT);
} else {
editor.putInt(PREF_PROXY_PORT, config.port);
}
if(TextUtils.isEmpty(config.username)) {
editor.remove(PREF_PROXY_USER);
} else {
editor.putString(PREF_PROXY_USER, config.username);
}
if(TextUtils.isEmpty(config.password)) {
editor.remove(PREF_PROXY_PASSWORD);
} else {
editor.putString(PREF_PROXY_PASSWORD, config.password);
}
editor.apply();
}
public static ProxyConfig getProxyConfig() {
Proxy.Type type = Proxy.Type.valueOf(prefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name()));
String host = prefs.getString(PREF_PROXY_HOST, null);
int port = prefs.getInt(PREF_PROXY_PORT, 0);
String username = prefs.getString(PREF_PROXY_USER, null);
String password = prefs.getString(PREF_PROXY_PASSWORD, null);
ProxyConfig config = new ProxyConfig(type, host, port, username, password);
return config;
}
public static boolean shouldResumeAfterCall() {
return prefs.getBoolean(PREF_RESUME_AFTER_CALL, true);
}

View File

@ -2,8 +2,10 @@ package de.danoeh.antennapod.core.service.download;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
@ -14,7 +16,10 @@ import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.concurrent.TimeUnit;
@ -23,6 +28,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
/**
@ -44,12 +50,15 @@ public class AntennapodHttpClient {
*/
public static synchronized OkHttpClient getHttpClient() {
if (httpClient == null) {
httpClient = newHttpClient();
}
return httpClient;
}
public static synchronized void reinit() {
httpClient = newHttpClient();
}
/**
* Creates a new HTTP client. Most users should just use
* getHttpClient() to get the standard AntennaPod client,
@ -69,13 +78,13 @@ public class AntennapodHttpClient {
client.networkInterceptors().add(chain -> {
Request request = chain.request();
Response response = chain.proceed(request);
if(response.code() == HttpURLConnection.HTTP_MOVED_PERM ||
if (response.code() == HttpURLConnection.HTTP_MOVED_PERM ||
response.code() == StatusLine.HTTP_PERM_REDIRECT) {
String location = response.header("Location");
if(location.startsWith("/")) { // URL is not absolute, but relative
if (location.startsWith("/")) { // URL is not absolute, but relative
URL url = request.url();
location = url.getProtocol() + "://" + url.getHost() + location;
} else if(!location.toLowerCase().startsWith("http://") &&
} else if (!location.toLowerCase().startsWith("http://") &&
!location.toLowerCase().startsWith("https://")) {
// Reference is relative to current path
URL url = request.url();
@ -106,6 +115,21 @@ public class AntennapodHttpClient {
client.setFollowRedirects(true);
client.setFollowSslRedirects(true);
ProxyConfig config = UserPreferences.getProxyConfig();
if (config.type != Proxy.Type.DIRECT) {
int port = config.port > 0 ? config.port : ProxyConfig.DEFAULT_PORT;
SocketAddress address = InetSocketAddress.createUnresolved(config.host, port);
Proxy proxy = new Proxy(config.type, address);
client.setProxy(proxy);
if (!TextUtils.isEmpty(config.username)) {
String credentials = Credentials.basic(config.username, config.password);
client.interceptors().add(chain -> {
Request request = chain.request().newBuilder()
.header("Proxy-Authorization", credentials).build();
return chain.proceed(request);
});
}
}
if(16 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 21) {
client.setSslSocketFactory(new CustomSslSocketFactory());
}

View File

@ -0,0 +1,32 @@
package de.danoeh.antennapod.core.service.download;
import android.support.annotation.Nullable;
import java.net.Proxy;
public class ProxyConfig {
public final Proxy.Type type;
@Nullable public final String host;
@Nullable public final int port;
@Nullable public final String username;
@Nullable public final String password;
public final static int DEFAULT_PORT = 8080;
public static ProxyConfig direct() {
return new ProxyConfig(Proxy.Type.DIRECT, null, 0, null, null);
}
public static ProxyConfig http(String host, int port, String username, String password) {
return new ProxyConfig(Proxy.Type.HTTP, host, port, username, password);
}
public ProxyConfig(Proxy.Type type, String host, int port, String username, String password) {
this.type = type;
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}
}

View File

@ -387,6 +387,8 @@
<string name="pref_sonic_title">Sonic media player</string>
<string name="pref_sonic_message">Use built-in sonic media player as a replacement for Android\'s native mediaplayer and Prestissimo</string>
<string name="pref_current_value">Current value: %1$s</string>
<string name="pref_proxy_title">Proxy</string>
<string name="pref_proxy_sum">Set a network proxy</string>
<!-- Auto-Flattr dialog -->
<string name="auto_flattr_enable">Enable automatic flattring</string>
@ -593,4 +595,17 @@
<string name="stereo_to_mono">Downmix: Stereo to mono</string>
<string name="sonic_only">Sonic only</string>
<!-- proxy settings -->
<string name="proxy_type_label">Type</string>
<string name="host_label">Host</string>
<string name="port_label">Port</string>
<string name="optional_hint">(Optional)</string>
<string name="proxy_test_label">Test</string>
<string name="proxy_checking">Checking&#8230;</string>
<string name="proxy_test_successful">Test successful</string>
<string name="proxy_test_failed">Test failed</string>
<string name="proxy_host_empty_error">Host must not be empty</string>
<string name="proxy_host_invalid_error">Host not valid UP or domain</string>
<string name="proxy_port_invalid_error">Port not valid</string>
</resources>