diff --git a/app/src/main/java/app/revanced/integrations/boostforreddit/FixSLinksPatch.java b/app/src/main/java/app/revanced/integrations/boostforreddit/FixSLinksPatch.java new file mode 100644 index 00000000..bbb0c60b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/boostforreddit/FixSLinksPatch.java @@ -0,0 +1,24 @@ +package app.revanced.integrations.boostforreddit; + +import com.rubenmayayo.reddit.ui.activities.WebViewActivity; + +import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch; + +/** @noinspection unused*/ +public class FixSLinksPatch extends BaseFixSLinksPatch { + static { + INSTANCE = new FixSLinksPatch(); + } + + private FixSLinksPatch() { + webViewActivityClass = WebViewActivity.class; + } + + public static boolean patchResolveSLink(String link) { + return INSTANCE.resolveSLink(link); + } + + public static void patchSetAccessToken(String accessToken) { + INSTANCE.setAccessToken(accessToken); + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/fixes/slink/BaseFixSLinksPatch.java b/app/src/main/java/app/revanced/integrations/shared/fixes/slink/BaseFixSLinksPatch.java new file mode 100644 index 00000000..ac2a9880 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/fixes/slink/BaseFixSLinksPatch.java @@ -0,0 +1,208 @@ +package app.revanced.integrations.shared.fixes.slink; + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.Objects; + +import static app.revanced.integrations.shared.Utils.getContext; + + +/** + * Base class to implement /s/ link resolution in 3rd party Reddit apps. + * <br> + * <br> + * Usage: + * <br> + * <br> + * An implementation of this class must have two static methods that are called by the app: + * <ul> + * <li>public static boolean patchResolveSLink(String link)</li> + * <li>public static void patchSetAccessToken(String accessToken)</li> + * </ul> + * The static methods must call the instance methods of the base class. + * <br> + * The singleton pattern can be used to access the instance of the class: + * <pre> + * {@code + * { + * INSTANCE = new FixSLinksPatch(); + * } + * } + * </pre> + * Set the app's web view activity class as a fallback to open /s/ links if the resolution fails: + * <pre> + * {@code + * private FixSLinksPatch() { + * webViewActivityClass = WebViewActivity.class; + * } + * } + * </pre> + * Hook the app's navigation handler to call this method before doing any of its own resolution: + * <pre> + * {@code + * public static boolean patchResolveSLink(Context context, String link) { + * return INSTANCE.resolveSLink(context, link); + * } + * } + * </pre> + * If this method returns true, the app should early return and not do any of its own resolution. + * <br> + * <br> + * Hook the app's access token so that this class can use it to resolve /s/ links: + * <pre> + * {@code + * public static void patchSetAccessToken(String accessToken) { + * INSTANCE.setAccessToken(access_token); + * } + * } + * </pre> + */ +public abstract class BaseFixSLinksPatch { + /** + * The class of the activity used to open links in a web view if resolving them fails. + */ + protected Class<? extends Activity> webViewActivityClass; + + /** + * The access token used to resolve the /s/ link. + */ + protected String accessToken; + + /** + * The URL that was trying to be resolved before the access token was set. + * If this is not null, the URL will be resolved right after the access token is set. + */ + protected String pendingUrl; + + /** + * The singleton instance of the class. + */ + protected static BaseFixSLinksPatch INSTANCE; + + public boolean resolveSLink(String link) { + switch (resolveLink(link)) { + case ACCESS_TOKEN_START: { + pendingUrl = link; + return true; + } + case DO_NOTHING: + return true; + default: + return false; + } + } + + private ResolveResult resolveLink(String link) { + Context context = getContext(); + if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) { + // A link ends with #bypass if it failed to resolve below. + // resolveLink is called with the same link again but this time with #bypass + // so that the link is opened in the app browser instead of trying to resolve it again. + if (link.endsWith("#bypass")) { + openInAppBrowser(context, link); + + return ResolveResult.DO_NOTHING; + } + + Logger.printDebug(() -> "Resolving " + link); + + if (accessToken == null) { + // This is not optimal. + // However, an accessToken is necessary to make an authenticated request to Reddit. + // in case Reddit has banned the IP - e.g. VPN. + Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + context.startActivity(startIntent); + + return ResolveResult.ACCESS_TOKEN_START; + } + + + Utils.runOnBackgroundThread(() -> { + String bypassLink = link + "#bypass"; + + String finalLocation = bypassLink; + try { + HttpURLConnection connection = getHttpURLConnection(link, accessToken); + connection.connect(); + String location = connection.getHeaderField("location"); + connection.disconnect(); + + Objects.requireNonNull(location, "Location is null"); + + finalLocation = location; + Logger.printDebug(() -> "Resolved " + link + " to " + location); + } catch (SocketTimeoutException e) { + Logger.printException(() -> "Timeout when trying to resolve " + link, e); + finalLocation = bypassLink; + } catch (Exception e) { + Logger.printException(() -> "Failed to resolve " + link, e); + finalLocation = bypassLink; + } finally { + Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation)); + startIntent.setPackage(context.getPackageName()); + startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(startIntent); + } + }); + + return ResolveResult.DO_NOTHING; + } + + return ResolveResult.CONTINUE; + } + + public void setAccessToken(String accessToken) { + Logger.printDebug(() -> "Setting access token"); + + this.accessToken = accessToken; + + // In case a link was trying to be resolved before access token was set. + // The link is resolved now, after the access token is set. + if (pendingUrl != null) { + String link = pendingUrl; + pendingUrl = null; + + Logger.printDebug(() -> "Opening pending URL"); + + resolveLink(link); + } + } + + private void openInAppBrowser(Context context, String link) { + Intent intent = new Intent(context, webViewActivityClass); + intent.putExtra("url", link); + context.startActivity(intent); + } + + @NonNull + private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException { + URL url = new URL(link); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(2000); + connection.setReadTimeout(2000); + + if (accessToken != null) { + Logger.printDebug(() -> "Setting access token to make /s/ request"); + + connection.setRequestProperty("Authorization", "Bearer " + accessToken); + } else { + Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null"); + } + + return connection; + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/fixes/slink/ResolveResult.java b/app/src/main/java/app/revanced/integrations/shared/fixes/slink/ResolveResult.java new file mode 100644 index 00000000..87985d63 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/fixes/slink/ResolveResult.java @@ -0,0 +1,10 @@ +package app.revanced.integrations.shared.fixes.slink; + +public enum ResolveResult { + // Let app handle rest of stuff + CONTINUE, + // Start app, to make it cache its access_token + ACCESS_TOKEN_START, + // Don't do anything - we started resolving + DO_NOTHING +} diff --git a/app/src/main/java/app/revanced/integrations/syncforreddit/FixSLinksPatch.java b/app/src/main/java/app/revanced/integrations/syncforreddit/FixSLinksPatch.java index a3c04ad6..c9857e70 100644 --- a/app/src/main/java/app/revanced/integrations/syncforreddit/FixSLinksPatch.java +++ b/app/src/main/java/app/revanced/integrations/syncforreddit/FixSLinksPatch.java @@ -1,42 +1,24 @@ package app.revanced.integrations.syncforreddit; -import android.os.StrictMode; -import app.revanced.integrations.shared.Logger; +import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity; -import java.net.HttpURLConnection; -import java.net.URL; +import app.revanced.integrations.shared.fixes.slink.BaseFixSLinksPatch; -public final class FixSLinksPatch { - public static String resolveSLink(String link) { - if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) { - Logger.printInfo(() -> "Resolving " + link); - try { - URL url = new URL(link); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setInstanceFollowRedirects(false); - connection.setRequestMethod("HEAD"); +/** @noinspection unused*/ +public class FixSLinksPatch extends BaseFixSLinksPatch { + static { + INSTANCE = new FixSLinksPatch(); + } - // Disable strict mode in order to allow network access on the main thread. - // This is not ideal, but it's the easiest solution for now. - final var currentPolicy = StrictMode.getThreadPolicy(); - StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); - StrictMode.setThreadPolicy(policy); + private FixSLinksPatch() { + webViewActivityClass = WebViewActivity.class; + } - connection.connect(); - String location = connection.getHeaderField("location"); - connection.disconnect(); + public static boolean patchResolveSLink(String link) { + return INSTANCE.resolveSLink(link); + } - // Restore the original strict mode policy. - StrictMode.setThreadPolicy(currentPolicy); - - Logger.printInfo(() -> "Resolved " + link + " -> " + location); - - return location; - } catch (Exception e) { - Logger.printException(() -> "Failed to resolve " + link, e); - } - } - - return link; + public static void patchSetAccessToken(String accessToken) { + INSTANCE.setAccessToken(accessToken); } } diff --git a/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java b/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java new file mode 100644 index 00000000..a33226a8 --- /dev/null +++ b/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java @@ -0,0 +1,6 @@ +package com.laurencedawson.reddit_sync.ui.activities; + +import android.app.Activity; + +public class WebViewActivity extends Activity { +} diff --git a/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java b/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java new file mode 100644 index 00000000..d0c58507 --- /dev/null +++ b/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java @@ -0,0 +1,6 @@ +package com.rubenmayayo.reddit.ui.activities; + +import android.app.Activity; + +public class WebViewActivity extends Activity { +}