From 2d77b1f11802da28cbcd87c9456bcc7ee3abf2a0 Mon Sep 17 00:00:00 2001
From: ByteHamster <ByteHamster@users.noreply.github.com>
Date: Sun, 17 Mar 2024 20:25:44 +0100
Subject: Remove dependency from :ui:glide to :core module (#6998)

---
 net/common/src/main/AndroidManifest.xml            |   6 +
 .../net/common/AntennapodHttpClient.java           | 108 ++++++++++++++++
 .../net/common/BasicAuthorizationInterceptor.java  |  80 ++++++++++++
 .../net/common/HttpCredentialEncoder.java          |  18 +++
 .../danoeh/antennapod/net/common/NetworkUtils.java | 144 +++++++++++++++++++++
 .../de/danoeh/antennapod/net/common/UriUtil.java   |  28 ++++
 .../net/common/UserAgentInterceptor.java           |  17 +++
 .../danoeh/antennapod/net/common/UriUtilTest.java  |  25 ++++
 8 files changed, 426 insertions(+)
 create mode 100644 net/common/src/main/AndroidManifest.xml
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java
 create mode 100644 net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java
 create mode 100644 net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java

(limited to 'net/common/src')

diff --git a/net/common/src/main/AndroidManifest.xml b/net/common/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1ffc73012
--- /dev/null
+++ b/net/common/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+</manifest>
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java
new file mode 100644
index 000000000..0a5231172
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/AntennapodHttpClient.java
@@ -0,0 +1,108 @@
+package de.danoeh.antennapod.net.common;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.ProxyConfig;
+import de.danoeh.antennapod.net.ssl.SslClientSetup;
+import okhttp3.Cache;
+import okhttp3.Credentials;
+import okhttp3.JavaNetCookieJar;
+import okhttp3.OkHttpClient;
+import java.io.File;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.SocketAddress;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides access to a HttpClient singleton.
+ */
+public class AntennapodHttpClient {
+    private static final String TAG = "AntennapodHttpClient";
+    private static final int CONNECTION_TIMEOUT = 10000;
+    private static final int READ_TIMEOUT = 30000;
+    private static final int MAX_CONNECTIONS = 8;
+    private static File cacheDirectory;
+    private static ProxyConfig proxyConfig;
+
+    private static volatile OkHttpClient httpClient = null;
+
+    private AntennapodHttpClient() {
+
+    }
+
+    /**
+     * Returns the HttpClient singleton.
+     */
+    public static synchronized OkHttpClient getHttpClient() {
+        if (httpClient == null) {
+            httpClient = newBuilder().build();
+        }
+        return httpClient;
+    }
+
+    public static synchronized void reinit() {
+        httpClient = newBuilder().build();
+    }
+
+    /**
+     * Creates a new HTTP client.  Most users should just use
+     * getHttpClient() to get the standard AntennaPod client,
+     * but sometimes it's necessary for others to have their own
+     * copy so that the clients don't share state.
+     * @return http client
+     */
+    @NonNull
+    public static OkHttpClient.Builder newBuilder() {
+        Log.d(TAG, "Creating new instance of HTTP client");
+
+        System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS));
+
+        OkHttpClient.Builder builder = new OkHttpClient.Builder();
+        builder.interceptors().add(new BasicAuthorizationInterceptor());
+        builder.networkInterceptors().add(new UserAgentInterceptor());
+
+        // set cookie handler
+        CookieManager cm = new CookieManager();
+        cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
+        builder.cookieJar(new JavaNetCookieJar(cm));
+
+        // set timeouts
+        builder.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS);
+        builder.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+        builder.writeTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
+        builder.cache(new Cache(cacheDirectory, 20L * 1000000)); // 20MB
+
+        // configure redirects
+        builder.followRedirects(true);
+        builder.followSslRedirects(true);
+
+        if (proxyConfig != null && proxyConfig.type != Proxy.Type.DIRECT && !TextUtils.isEmpty(proxyConfig.host)) {
+            int port = proxyConfig.port > 0 ? proxyConfig.port : ProxyConfig.DEFAULT_PORT;
+            SocketAddress address = InetSocketAddress.createUnresolved(proxyConfig.host, port);
+            builder.proxy(new Proxy(proxyConfig.type, address));
+            if (!TextUtils.isEmpty(proxyConfig.username) && proxyConfig.password != null) {
+                builder.proxyAuthenticator((route, response) -> {
+                    String credentials = Credentials.basic(proxyConfig.username, proxyConfig.password);
+                    return response.request().newBuilder()
+                            .header("Proxy-Authorization", credentials)
+                            .build();
+                });
+            }
+        }
+
+        SslClientSetup.installCertificates(builder);
+        return builder;
+    }
+
+    public static void setCacheDirectory(File cacheDirectory) {
+        AntennapodHttpClient.cacheDirectory = cacheDirectory;
+    }
+
+    public static void setProxyConfig(ProxyConfig proxyConfig) {
+        AntennapodHttpClient.proxyConfig = proxyConfig;
+    }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java
new file mode 100644
index 000000000..8e7b9a4f4
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/BasicAuthorizationInterceptor.java
@@ -0,0 +1,80 @@
+package de.danoeh.antennapod.net.common;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.download.DownloadRequest;
+import de.danoeh.antennapod.net.common.HttpCredentialEncoder;
+import de.danoeh.antennapod.net.common.UriUtil;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.List;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class BasicAuthorizationInterceptor implements Interceptor {
+    private static final String TAG = "BasicAuthInterceptor";
+    private static final String HEADER_AUTHORIZATION = "Authorization";
+
+    @Override
+    @NonNull
+    public Response intercept(Chain chain) throws IOException {
+        Request request = chain.request();
+
+        Response response = chain.proceed(request);
+
+        if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
+            return response;
+        }
+
+        Request.Builder newRequest = request.newBuilder();
+        if (!TextUtils.equals(response.request().url().toString(), request.url().toString())) {
+            // Redirect detected. OkHTTP does not re-add the headers on redirect, so calling the new location directly.
+            newRequest.url(response.request().url());
+
+            List<String> authorizationHeaders = request.headers().values(HEADER_AUTHORIZATION);
+            if (!authorizationHeaders.isEmpty() && !TextUtils.isEmpty(authorizationHeaders.get(0))) {
+                // Call already had authorization headers. Try again with the same credentials.
+                newRequest.header(HEADER_AUTHORIZATION, authorizationHeaders.get(0));
+                return chain.proceed(newRequest.build());
+            }
+        }
+
+        String userInfo = null;
+        if (request.tag() instanceof DownloadRequest) {
+            DownloadRequest downloadRequest = (DownloadRequest) request.tag();
+            userInfo = UriUtil.getURIFromRequestUrl(downloadRequest.getSource()).getUserInfo();
+            if (TextUtils.isEmpty(userInfo)
+                    && (!TextUtils.isEmpty(downloadRequest.getUsername())
+                        || !TextUtils.isEmpty(downloadRequest.getPassword()))) {
+                userInfo = downloadRequest.getUsername() + ":" + downloadRequest.getPassword();
+            }
+        }
+
+        if (TextUtils.isEmpty(userInfo)) {
+            Log.d(TAG, "no credentials for '" + request.url() + "'");
+            return response;
+        }
+
+        if (!userInfo.contains(":")) {
+            Log.d(TAG, "Invalid credentials for '" + request.url() + "'");
+            return response;
+        }
+        String username = userInfo.substring(0, userInfo.indexOf(':'));
+        String password = userInfo.substring(userInfo.indexOf(':') + 1);
+
+        Log.d(TAG, "Authorization failed, re-trying with ISO-8859-1 encoded credentials");
+        newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "ISO-8859-1"));
+        response = chain.proceed(newRequest.build());
+
+        if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
+            return response;
+        }
+
+        Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoded credentials");
+        newRequest.header(HEADER_AUTHORIZATION, HttpCredentialEncoder.encode(username, password, "UTF-8"));
+        return chain.proceed(newRequest.build());
+    }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java
new file mode 100644
index 000000000..9b2063ce6
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/HttpCredentialEncoder.java
@@ -0,0 +1,18 @@
+package de.danoeh.antennapod.net.common;
+
+import android.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+
+public abstract class HttpCredentialEncoder {
+    public static String encode(String username, String password, String charset) {
+        try {
+            String credentials = username + ":" + password;
+            byte[] bytes = credentials.getBytes(charset);
+            String encoded = Base64.encodeToString(bytes, Base64.NO_WRAP);
+            return "Basic " + encoded;
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+    }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java
new file mode 100644
index 000000000..179c4e13e
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/NetworkUtils.java
@@ -0,0 +1,144 @@
+package de.danoeh.antennapod.net.common;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+public class NetworkUtils {
+    private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}";
+
+    private NetworkUtils(){}
+
+    private static Context context;
+
+    public static void init(Context context) {
+        NetworkUtils.context = context;
+    }
+
+    public static boolean isAutoDownloadAllowed() {
+        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+        if (networkInfo == null) {
+            return false;
+        }
+        if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+            if (UserPreferences.isEnableAutodownloadWifiFilter()) {
+                return isInAllowedWifiNetwork();
+            } else {
+                return !isNetworkMetered();
+            }
+        } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
+            return true;
+        } else {
+            return UserPreferences.isAllowMobileAutoDownload() || !NetworkUtils.isNetworkRestricted();
+        }
+    }
+
+    public static boolean networkAvailable() {
+        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo info = cm.getActiveNetworkInfo();
+        return info != null && info.isConnected();
+    }
+
+    public static boolean isEpisodeDownloadAllowed() {
+        return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkRestricted();
+    }
+
+    public static boolean isEpisodeHeadDownloadAllowed() {
+        // It is not an image but it is a similarly tiny request
+        // that is probably not even considered a download by most users
+        return isImageAllowed();
+    }
+
+    public static boolean isImageAllowed() {
+        return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkRestricted();
+    }
+
+    public static boolean isStreamingAllowed() {
+        return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkRestricted();
+    }
+
+    public static boolean isFeedRefreshAllowed() {
+        return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkRestricted();
+    }
+
+    public static boolean isNetworkRestricted() {
+        return isNetworkMetered() || isNetworkCellular();
+    }
+
+    private static boolean isNetworkMetered() {
+        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        return connManager.isActiveNetworkMetered();
+    }
+
+    public static boolean isVpnOverWifi() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            return false;
+        }
+        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkCapabilities capabilities = connManager.getNetworkCapabilities(connManager.getActiveNetwork());
+        return capabilities != null
+                && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+                && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
+    }
+
+    private static boolean isNetworkCellular() {
+        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (Build.VERSION.SDK_INT >= 23) {
+            Network network = connManager.getActiveNetwork();
+            if (network == null) {
+                return false; // Nothing connected
+            }
+            NetworkInfo info = connManager.getNetworkInfo(network);
+            if (info == null) {
+                return true; // Better be safe than sorry
+            }
+            NetworkCapabilities capabilities = connManager.getNetworkCapabilities(network);
+            if (capabilities == null) {
+                return true; // Better be safe than sorry
+            }
+            return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+        } else {
+            // if the default network is a VPN,
+            // this method will return the NetworkInfo for one of its underlying networks
+            NetworkInfo info = connManager.getActiveNetworkInfo();
+            if (info == null) {
+                return false; // Nothing connected
+            }
+            //noinspection deprecation
+            return info.getType() == ConnectivityManager.TYPE_MOBILE;
+        }
+    }
+
+    private static boolean isInAllowedWifiNetwork() {
+        WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+        List<String> selectedNetworks = Arrays.asList(UserPreferences.getAutodownloadSelectedNetworks());
+        return selectedNetworks.contains(Integer.toString(wm.getConnectionInfo().getNetworkId()));
+    }
+
+    public static boolean wasDownloadBlocked(Throwable throwable) {
+        String message = throwable.getMessage();
+        if (message != null) {
+            Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS);
+            Matcher matcher = pattern.matcher(message);
+            if (matcher.find()) {
+                String ip = matcher.group();
+                return ip.startsWith("127.") || ip.startsWith("0.");
+            }
+        }
+        if (throwable.getCause() != null) {
+            return wasDownloadBlocked(throwable.getCause());
+        }
+        return false;
+    }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java
new file mode 100644
index 000000000..63fc087d6
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/UriUtil.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.net.common;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Utility methods for dealing with URL encoding.
+ */
+public class UriUtil {
+    private UriUtil() {}
+
+    public static URI getURIFromRequestUrl(String source) {
+        // try without encoding the URI
+        try {
+            return new URI(source);
+        } catch (URISyntaxException ignore) {
+            System.out.println("Source is not encoded, encoding now");
+        }
+        try {
+            URL url = new URL(source);
+            return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
+        } catch (MalformedURLException | URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+}
diff --git a/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java b/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java
new file mode 100644
index 000000000..7a61aa070
--- /dev/null
+++ b/net/common/src/main/java/de/danoeh/antennapod/net/common/UserAgentInterceptor.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.net.common;
+
+import okhttp3.Interceptor;
+import okhttp3.Response;
+
+import java.io.IOException;
+
+public class UserAgentInterceptor implements Interceptor {
+    public static String USER_AGENT = "AntennaPod/0.0.0";
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        return chain.proceed(chain.request().newBuilder()
+                .header("User-Agent", USER_AGENT)
+                .build());
+    }
+}
diff --git a/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java b/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java
new file mode 100644
index 000000000..32a856284
--- /dev/null
+++ b/net/common/src/test/java/de/danoeh/antennapod/net/common/UriUtilTest.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.net.common;
+
+import de.danoeh.antennapod.net.common.UriUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test class for URIUtil
+ */
+public class UriUtilTest {
+
+    @Test
+    public void testGetURIFromRequestUrlShouldNotEncode() {
+        final String testUrl = "http://example.com/this%20is%20encoded";
+        assertEquals(testUrl, UriUtil.getURIFromRequestUrl(testUrl).toString());
+    }
+
+    @Test
+    public void testGetURIFromRequestUrlShouldEncode() {
+        final String testUrl = "http://example.com/this is not encoded";
+        final String expected = "http://example.com/this%20is%20not%20encoded";
+        assertEquals(expected, UriUtil.getURIFromRequestUrl(testUrl).toString());
+    }
+}
-- 
cgit v1.2.3