diff --git a/app/src/main/java/app/revanced/integrations/instagram/patches/download/DownloadPatch.java b/app/src/main/java/app/revanced/integrations/instagram/patches/download/DownloadPatch.java new file mode 100644 index 0000000000..6bf84ebb61 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/instagram/patches/download/DownloadPatch.java @@ -0,0 +1,193 @@ +package app.revanced.integrations.instagram.patches.download; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.widget.Toast; +import app.revanced.integrations.shared.Utils; +import com.instagram.api.schemas.MediaOptionStyle; +import com.instagram.model.mediasize.ExtendedImageUrl; + +import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; +import java.util.Objects; + +@SuppressWarnings("unused") +public class DownloadPatch { + // To be added in revanced patches + public static String feedOptionItemIconClassName() { + return ""; + } + + public static String feedItemClassName() { + return ""; + } + + public static String mediaListSourceClass() { + return ""; + } + + public static String listFieldName() { + return ""; + } + + public static Class feedItemIconClass; + public static Class feedItem; + public static Class mediaListSourceClass; + + static { + try { + feedItemIconClass = Class.forName(feedOptionItemIconClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + try { + feedItem = Class.forName(feedItemClassName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + + try { + mediaListSourceClass = Class.forName(mediaListSourceClass()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + public static void addDownloadButton(List items) throws IllegalAccessException, InstantiationException, + NoSuchMethodException, InvocationTargetException, NoSuchFieldException { + Object item = feedItem + .getConstructor(MediaOptionStyle.class, feedItemIconClass, CharSequence.class) + // TODO dynamically get the Download icon field + .newInstance(MediaOptionStyle.A05, feedItemIconClass.getField("A0Z").get(feedItemIconClass), + "Download"); + + items.add(item); + } + + public static void downloadPost(Object media, int index, Object p3, Activity act) throws URISyntaxException, NoSuchFieldException, IllegalAccessException, MalformedURLException, InvocationTargetException { + Field mediaField = null; + + // Get media field + for (Field field : media.getClass().getFields()) { + if (field.getType() == mediaListSourceClass.getInterfaces()[0]) { + mediaField = field; + } + } + + assert mediaField != null; + Object _mediaList = mediaField.get(media); + _mediaList = mediaListSourceClass.cast(_mediaList); + + List mediaList = (List) _mediaList.getClass().getField(listFieldName()).get(_mediaList); + + if (mediaList != null) { + downloadDialog(mediaList, index, act); + } else { + downloadMedia(media); + } + } + + public static void downloadDialog(List mediaList, int index, Activity activity) { + // TODO use instagram dialog + AlertDialog.Builder d = new AlertDialog.Builder(activity) + .setTitle("Download") + .setItems(new String[]{"Current", "Download all"}, (dialogInterface, i) -> { + try { + if (i == 0) { + downloadMedia(mediaList.get(index)); + } else { + for (Object media : mediaList) { + downloadMedia(media); + } + } + } catch (URISyntaxException | MalformedURLException | InvocationTargetException | + IllegalAccessException e) { + e.printStackTrace(); + Toast.makeText(Utils.getContext(), "Download failed", Toast.LENGTH_SHORT).show(); + } + }); + d.show(); + } + + public static void downloadMedia(Object media) throws URISyntaxException, MalformedURLException, InvocationTargetException, IllegalAccessException { + if (isVideo(media)) { + startDownload(getVideoLink(media), Environment.DIRECTORY_MOVIES, "Instagram"); + } else { + startDownload(getPhotoLink(media), Environment.DIRECTORY_PICTURES, "Instagram"); + } + } + + public static void startDownload(String url, String publicFolder, String subpath) throws URISyntaxException, MalformedURLException { + URL link = new URL(url); + + String filename = link.getPath().split("/")[link.getPath().split("/").length - 1]; + String publicFolderDirectory = Environment.DIRECTORY_PICTURES; + String subPath =subpath + "/" + filename; + + File imageFile = new File(Environment.getExternalStoragePublicDirectory(publicFolderDirectory), subPath); + + if (imageFile.exists()) { + return; + } + + DownloadManager mgr = (DownloadManager) Utils.getContext().getSystemService(Context.DOWNLOAD_SERVICE); + + mgr.enqueue(new DownloadManager.Request(Uri.parse(link.toURI().toString())) + .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | + DownloadManager.Request.NETWORK_MOBILE) + .setAllowedOverRoaming(false) + .setTitle(filename) + .setDescription("Downloading...") + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + .setDestinationInExternalPublicDir(publicFolderDirectory, + subPath)); + + Toast.makeText(Utils.getContext(), "Download completed", Toast.LENGTH_SHORT).show(); + } + + private static String getVideoLink(Object media) { + return ""; + } + + public static String getPhotoLink(Object media) throws InvocationTargetException, IllegalAccessException { + Method getExtendedImageUrl = null; + URL url = null; + + for (Method method : media.getClass().getMethods()) { + if (method.getParameterTypes().length == 0) { + continue; + } + + if (method.getReturnType() == ExtendedImageUrl.class + && method.getParameterTypes()[0] == Context.class) { + getExtendedImageUrl = method; + } + } + + assert getExtendedImageUrl != null; + return ((ExtendedImageUrl) Objects + .requireNonNull(getExtendedImageUrl.invoke(media, Utils.getContext()))) + .getUrl(); + } + + public static boolean isVideo(Object media) { + return true; + } + + public static void print(Object message) { + System.out.println("BRUH: " + message); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/shared/Logger.java b/app/src/main/java/app/revanced/integrations/shared/Logger.java index f8c5c87eee..766f2b5009 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Logger.java +++ b/app/src/main/java/app/revanced/integrations/shared/Logger.java @@ -136,6 +136,14 @@ public static void printException(@NonNull LogMessage message, @Nullable Throwab } } + /** + * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. + * Normally this method should not be used. + */ + public static void initializationInfo(@NonNull Class callingClass, @NonNull String message) { + Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message); + } + /** * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#context} may not be initialized. * Always logs even if Debugging is not enabled. diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index cac761c7f5..beebb24f29 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -259,6 +259,20 @@ public static void setClipboard(@NonNull String text) { clipboard.setPrimaryClip(clip); } + public static void setContext(Context appContext) { + context = appContext; + // In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies. + // Calling the regular printDebug method here can cause a Settings context null pointer exception, + // even though the context is already set before the call. + // + // The initialization logger methods do not directly or indirectly + // reference the Context or any Settings and are unaffected by this problem. + // + // Info level also helps debug if a patch hook is called before + // the context is set since debug logging is off by default. + Logger.initializationInfo(Utils.class, "Set context: " + appContext); + } + public static boolean isTablet() { return context.getResources().getConfiguration().smallestScreenWidthDp >= 600; } diff --git a/app/src/main/java/app/revanced/integrations/twitter/patches/FeatureSwitchPatch.java b/app/src/main/java/app/revanced/integrations/twitter/patches/FeatureSwitchPatch.java index 0576c914c4..6da40da56f 100644 --- a/app/src/main/java/app/revanced/integrations/twitter/patches/FeatureSwitchPatch.java +++ b/app/src/main/java/app/revanced/integrations/twitter/patches/FeatureSwitchPatch.java @@ -2,6 +2,7 @@ import java.util.*; +import android.util.Log; import app.revanced.integrations.twitter.Pref; import app.revanced.integrations.twitter.Utils; import app.revanced.integrations.twitter.settings.Settings; @@ -38,9 +39,11 @@ private static void immersivePlayer() { } public static Object flagInfo(String flag, Object def) { +// Log.d("BRUH", "name: "+flag+" value: "+def.toString()+" type: "+def.getClass().getName()); try { if (FLAGS.containsKey(flag)) { Object val = FLAGS.get(flag); +// Log.d("BRUH", "name: "+flag+" value: "+val+" type: "+val.getClass().getName()); return val; } } catch (Exception ex) { @@ -58,5 +61,11 @@ public static void load() { addFlag(item[0], Boolean.valueOf(item[1])); } } + + addFlag("timeline_auto_refresh_on_foreground_timeout_millis", 99999.0); + addFlag("recent_search_limit_count", 10); + addFlag("home_timeline_start_at_top_min_background_minutes", 99999.0); + addFlag("home_timeline_start_at_top_warm_start_min_background_minutes", 99999.0); + addFlag("home_timeline_start_at_top_timeout_length", 99999.0); } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/twitter/patches/customise/NavBar.java b/app/src/main/java/app/revanced/integrations/twitter/patches/customise/NavBar.java new file mode 100644 index 0000000000..34e2d44ba5 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/twitter/patches/customise/NavBar.java @@ -0,0 +1,17 @@ +package app.revanced.integrations.twitter.patches.customise; + +import android.util.Log; + +import java.util.List; + +public class NavBar { + public static void printList(List list) { + for (Object o: list) { + print(o.getClass().getName()); + } + } + + public static void print(String a) { + System.out.println("BRUH"+ a); + } +} diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts index d16c19b7a5..fb53193c05 100644 --- a/stub/build.gradle.kts +++ b/stub/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.library) + id("org.jetbrains.kotlin.android") } android { @@ -24,4 +25,10 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = "11" + } +} +dependencies { + implementation("androidx.core:core-ktx:+") } diff --git a/stub/src/main/java/com/instagram/api/schemas/MediaOptionStyle.java b/stub/src/main/java/com/instagram/api/schemas/MediaOptionStyle.java new file mode 100644 index 0000000000..a06a655f5b --- /dev/null +++ b/stub/src/main/java/com/instagram/api/schemas/MediaOptionStyle.java @@ -0,0 +1,9 @@ +package com.instagram.api.schemas; + +public enum MediaOptionStyle { +// TODO dynamically get the normal media type field + A05("normal"); + + MediaOptionStyle(String mediaOptionStyleUnspecified) { + } +} \ No newline at end of file diff --git a/stub/src/main/java/com/instagram/model/mediasize/ExtendedImageUrl.java b/stub/src/main/java/com/instagram/model/mediasize/ExtendedImageUrl.java new file mode 100644 index 0000000000..38072249c4 --- /dev/null +++ b/stub/src/main/java/com/instagram/model/mediasize/ExtendedImageUrl.java @@ -0,0 +1,7 @@ +package com.instagram.model.mediasize; + +public class ExtendedImageUrl { + public String getUrl() { + return ""; + } +}