Compare commits

..

3 Commits

Author SHA1 Message Date
oSumAtrIX
9dca7681ea Merge branch 'dev' into feat/universal-icon-patch
# Conflicts:
#	patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt
2024-12-05 23:12:56 +01:00
oSumAtrIX
5c1905f937 finish first working version 2024-12-01 09:15:02 +01:00
oSumAtrIX
a5a14ac31a Init 2024-11-29 03:18:57 +01:00
49 changed files with 596 additions and 927 deletions

View File

@@ -1,45 +1,3 @@
## [5.2.4-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.2...v5.2.4-dev.3) (2024-12-07)
### Bug Fixes
* **YouTube - Spoof video streams:** Enable opus codec by updating iOS client version ([#4063](https://github.com/ReVanced/revanced-patches/issues/4063)) ([0af156f](https://github.com/ReVanced/revanced-patches/commit/0af156f18972c5f089af4bb69824968d2a47d18f))
## [5.2.4-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.1...v5.2.4-dev.2) (2024-12-07)
### Bug Fixes
* **Sync for Reddit:** Fix patches by using correct extension name ([030093e](https://github.com/ReVanced/revanced-patches/commit/030093e913aab3fab43935eedbaeba0f6c0491bb))
## [5.2.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.2.4-dev.1) (2024-12-07)
### Bug Fixes
* **Twitter:** Merge correct extension by depending on correct extension patch ([8281cf6](https://github.com/ReVanced/revanced-patches/commit/8281cf6a3eead8cc25a277371e0b0ab2be982497))
## [5.2.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3) (2024-12-06)
### Bug Fixes
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
## [5.2.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3-dev.1) (2024-12-06)
### Bug Fixes
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
## [5.2.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2) (2024-12-06)
### Bug Fixes
* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b))
## [5.2.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2-dev.1) (2024-12-05)

View File

@@ -1,4 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(libs.annotation)
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,20 +0,0 @@
package app.revanced.extension.music.announcements;
import android.app.Activity;
import android.os.Build;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.announcements.BaseAnnouncementsPatch;
@SuppressWarnings("unused")
public class AnnouncementsPatch extends BaseAnnouncementsPatch {
private static final AnnouncementsPatch INSTANCE = new AnnouncementsPatch();
private AnnouncementsPatch() {
super("music");
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void showAnnouncement(final Activity context) {
INSTANCE._showAnnouncement(context);
}
}

View File

@@ -1,172 +0,0 @@
package app.revanced.extension.shared.announcements;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Build;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
import org.json.JSONArray;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.LocalDateTime;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.announcements.requests.AnnouncementsRoutes;
import app.revanced.extension.shared.settings.BaseSettings;
public abstract class BaseAnnouncementsPatch {
private final AnnouncementsRoutes announcementsRoutes;
public BaseAnnouncementsPatch(String tag) {
this.announcementsRoutes = new AnnouncementsRoutes(tag);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private boolean isLatestAlready() throws IOException {
HttpURLConnection connection =
announcementsRoutes.getAnnouncementsConnectionFromRoute(announcementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS);
Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL());
try {
// Do not show the announcement if the request failed.
if (connection.getResponseCode() != 200) {
if (BaseSettings.ANNOUNCEMENT_LAST_ID.isSetToDefault())
return true;
BaseSettings.ANNOUNCEMENT_LAST_ID.resetToDefault();
Utils.showToastLong(str("revanced_announcements_connection_failed"));
return true;
}
} catch (IOException ex) {
Logger.printException(() -> "Could not connect to announcements provider", ex);
return true;
}
var jsonString = Requester.parseStringAndDisconnect(connection);
// Parse the ID. Fall-back to raw string if it fails.
int id = BaseSettings.ANNOUNCEMENT_LAST_ID.defaultValue;
try {
final var announcementIds = new JSONArray(jsonString);
id = announcementIds.getJSONObject(0).getInt("id");
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement IDs", ex);
}
// Do not show the announcement, if the last announcement id is the same as the current one.
return BaseSettings.ANNOUNCEMENT_LAST_ID.get() == id;
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void _showAnnouncement(final Activity context) {
if (!BaseSettings.ANNOUNCEMENTS.get()) return;
// Check if there is internet connection
if (!Utils.isNetworkConnected()) return;
Utils.runOnBackgroundThread(() -> {
try {
if (isLatestAlready()) return;
HttpURLConnection connection = announcementsRoutes
.getAnnouncementsConnectionFromRoute(announcementsRoutes.GET_LATEST_ANNOUNCEMENTS);
Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL());
var jsonString = Requester.parseStringAndDisconnect(connection);
// Parse the announcement. Fall-back to raw string if it fails.
int id = BaseSettings.ANNOUNCEMENT_LAST_ID.defaultValue;
String title;
String message;
LocalDateTime archivedAt = LocalDateTime.MAX;
Level level = Level.INFO;
try {
final var announcement = new JSONArray(jsonString).getJSONObject(0);
id = announcement.getInt("id");
title = announcement.getString("title");
message = announcement.getString("content");
if (!announcement.isNull("archived_at")) {
archivedAt = LocalDateTime.parse(announcement.getString("archived_at"));
}
if (!announcement.isNull("level")) {
level = Level.fromInt(announcement.getInt("level"));
}
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
title = "Announcement";
message = jsonString;
}
// If the announcement is archived, do not show it.
if (archivedAt.isBefore(LocalDateTime.now())) {
BaseSettings.ANNOUNCEMENT_LAST_ID.save(id);
return;
}
int finalId = id;
final var finalTitle = title;
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
final Level finalLevel = level;
Utils.runOnMainThread(() -> {
// Show the announcement.
var alert = new AlertDialog.Builder(context)
.setTitle(finalTitle)
.setMessage(finalMessage)
.setIcon(finalLevel.icon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
BaseSettings.ANNOUNCEMENT_LAST_ID.save(finalId);
dialog.dismiss();
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
dialog.dismiss();
})
.setCancelable(false)
.create();
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
// Make links clickable.
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
});
} catch (Exception e) {
final var message = "Failed to get announcement";
Logger.printException(() -> message, e);
}
});
}
// TODO: Use better icons.
private enum Level {
INFO(android.R.drawable.ic_dialog_info),
WARNING(android.R.drawable.ic_dialog_alert),
SEVERE(android.R.drawable.ic_dialog_alert);
public final int icon;
Level(int icon) {
this.icon = icon;
}
public static Level fromInt(int value) {
return values()[Math.min(value, values().length - 1)];
}
}
}

View File

@@ -1,23 +0,0 @@
package app.revanced.extension.shared.announcements.requests;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import java.io.IOException;
import java.net.HttpURLConnection;
import static app.revanced.extension.shared.requests.Route.Method.GET;
public class AnnouncementsRoutes {
public final Route GET_LATEST_ANNOUNCEMENTS;
public final Route GET_LATEST_ANNOUNCEMENT_IDS;
public AnnouncementsRoutes(String tag) {
this.GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=" + tag);
this.GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/ids?tag=" + tag);
}
public HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute("https://api.revanced.app/v4", route, params);
}
}

View File

@@ -16,7 +16,4 @@ public class BaseSettings {
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
}

View File

@@ -1,20 +1,172 @@
package app.revanced.extension.youtube.patches.announcements;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Build;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.announcements.BaseAnnouncementsPatch;
import org.json.JSONArray;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.time.LocalDateTime;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AnnouncementsPatch extends BaseAnnouncementsPatch {
private static final AnnouncementsPatch INSTANCE = new AnnouncementsPatch();
public final class AnnouncementsPatch {
private AnnouncementsPatch() {
super("youtube");
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static boolean isLatestAlready() throws IOException {
HttpURLConnection connection =
AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS);
Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL());
try {
// Do not show the announcement if the request failed.
if (connection.getResponseCode() != 200) {
if (Settings.ANNOUNCEMENT_LAST_ID.isSetToDefault())
return true;
Settings.ANNOUNCEMENT_LAST_ID.resetToDefault();
Utils.showToastLong(str("revanced_announcements_connection_failed"));
return true;
}
} catch (IOException ex) {
Logger.printException(() -> "Could not connect to announcements provider", ex);
return true;
}
var jsonString = Requester.parseStringAndDisconnect(connection);
// Parse the ID. Fall-back to raw string if it fails.
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
try {
final var announcementIds = new JSONArray(jsonString);
id = announcementIds.getJSONObject(0).getInt("id");
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement IDs", ex);
}
// Do not show the announcement, if the last announcement id is the same as the current one.
return Settings.ANNOUNCEMENT_LAST_ID.get() == id;
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void showAnnouncement(final Activity context) {
INSTANCE._showAnnouncement(context);
if (!Settings.ANNOUNCEMENTS.get()) return;
// Check if there is internet connection
if (!Utils.isNetworkConnected()) return;
Utils.runOnBackgroundThread(() -> {
try {
if (isLatestAlready()) return;
HttpURLConnection connection = AnnouncementsRoutes
.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENTS);
Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL());
var jsonString = Requester.parseStringAndDisconnect(connection);
// Parse the announcement. Fall-back to raw string if it fails.
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
String title;
String message;
LocalDateTime archivedAt = LocalDateTime.MAX;
Level level = Level.INFO;
try {
final var announcement = new JSONArray(jsonString).getJSONObject(0);
id = announcement.getInt("id");
title = announcement.getString("title");
message = announcement.getString("content");
if (!announcement.isNull("archived_at")) {
archivedAt = LocalDateTime.parse(announcement.getString("archived_at"));
}
if (!announcement.isNull("level")) {
level = Level.fromInt(announcement.getInt("level"));
}
} catch (Throwable ex) {
Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex);
title = "Announcement";
message = jsonString;
}
// If the announcement is archived, do not show it.
if (archivedAt.isBefore(LocalDateTime.now())) {
Settings.ANNOUNCEMENT_LAST_ID.save(id);
return;
}
int finalId = id;
final var finalTitle = title;
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
final Level finalLevel = level;
Utils.runOnMainThread(() -> {
// Show the announcement.
var alert = new AlertDialog.Builder(context)
.setTitle(finalTitle)
.setMessage(finalMessage)
.setIcon(finalLevel.icon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
dialog.dismiss();
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
dialog.dismiss();
})
.setCancelable(false)
.create();
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
// Make links clickable.
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
});
} catch (Exception e) {
final var message = "Failed to get announcement";
Logger.printException(() -> message, e);
}
});
}
// TODO: Use better icons.
private enum Level {
INFO(android.R.drawable.ic_dialog_info),
WARNING(android.R.drawable.ic_dialog_alert),
SEVERE(android.R.drawable.ic_dialog_alert);
public final int icon;
Level(int icon) {
this.icon = icon;
}
public static Level fromInt(int value) {
return values()[Math.min(value, values().length - 1)];
}
}
}

View File

@@ -0,0 +1,22 @@
package app.revanced.extension.youtube.patches.announcements.requests;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import java.io.IOException;
import java.net.HttpURLConnection;
import static app.revanced.extension.shared.requests.Route.Method.GET;
public class AnnouncementsRoutes {
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=youtube");
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=youtube");
private AnnouncementsRoutes() {
}
public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params);
}
}

View File

@@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
public enum ClientType {
// Specific purpose for age restricted, or private videos, because the iOS client is not logged in.
// https://dumps.tadiphone.dev/dumps/oculus/eureka
ANDROID_VR(28,
"Quest 3",
"12",
@@ -20,6 +19,7 @@ public enum ClientType {
true
),
// Specific for kids videos.
// https://dumps.tadiphone.dev/dumps/oculus/eureka
IOS(5,
// iPhone 15 supports AV1 hardware decoding.
// Only use if this Android device also has hardware decoding.
@@ -31,12 +31,12 @@ public enum ClientType {
? "17.5.1.21F90"
: "13.7.17H35",
allowVP9()
? "com.google.ios.youtube/19.47.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
: "com.google.ios.youtube/19.47.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
null,
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
"19.47.7",
"19.10.7",
"IOS",
false
);

View File

@@ -271,10 +271,12 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
"revanced_spoof_device_dimensions_user_dialog_message");
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true,"revanced_spoof_video_streams_user_dialog_message");
public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true,
"revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofVideoStreamsPatch.ForceiOSAVCAvailability());
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS));
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.2.4-dev.3
version = 5.2.2-dev.1

View File

@@ -1,10 +1,9 @@
public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt {
public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public final class app/revanced/patches/all/layout/branding/IconPatchKt {
public static final fun getChangeIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/all/misc/announcements/AnnouncementsPatchKt {
public static final fun announcementsPatch (Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun announcementsPatch$default (Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt {
public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatchKt {
@@ -309,10 +308,6 @@ public final class app/revanced/patches/music/misc/androidauto/BypassCertificate
public static final fun getBypassCertificateChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/music/misc/announcements/AnnouncementsPatchKt {
public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt {
public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View File

@@ -0,0 +1,155 @@
package app.revanced.patches.all.layout.branding
import app.revanced.patcher.patch.*
import app.revanced.util.getNode
import app.revanced.util.inputStreamFromBundledResource
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
private const val FULL_ICON = 0
private const val ROUND_ICON = 1
private const val BACKGROUND_ICON = 2
private const val FOREGROUND_ICON = 3
private const val MONOCHROME_ICON = 4
val changeIconPatch = resourcePatch(
name = "Change icon",
description = "Changes the app icon to a custom icon. By default, the ReVanced icon is used.",
use = false,
) {
val revancedIconOptionValue = emptyList<String>() // Empty list == ReVanced icon.
val pixelDensities = setOf(
"mdpi",
"hdpi",
"xhdpi",
"xxhdpi",
"xxxhdpi",
)
val iconOptions = pixelDensities.associateWith { pixelDensity ->
stringsOption(
key = "${pixelDensity}Icons",
default = revancedIconOptionValue,
values = mapOf("ReVanced logo" to revancedIconOptionValue),
title = "Icons (Pixel density: $pixelDensity)",
description = buildString {
appendLine("Provide paths to the following icons for pixel density $pixelDensity (PNG, JPG, WEBP, or vector drawable XML):")
appendLine("1. Launcher icon (required)")
appendLine("2. Round icon (optional, Android 7+)")
appendLine("\nYou can use adaptive icons (Android 8+) by providing the following additional icons:")
appendLine("\n3. Background icon (optional)")
appendLine("4. Foreground icon (optional)")
appendLine("5. Monochrome icon (optional, Android 13+")
appendLine("\nIcons must be provided in the same order as listed above. Missing optional icons can be skipped by leaving the field empty.")
appendLine("\nYou can create custom icon sets at https://icon.kitchen.")
},
required = true,
)
}
execute {
val firstPixelDensity = pixelDensities.first()
fun patchIcon(
getIcon: (String, Int) -> String?,
readIcon: (String) -> InputStream,
) {
// Any density, as the user should provide the icons for all densities.
// region Change the app icon in the AndroidManifest.xml file.
// If a round icon is provided, set the android:roundIcon attribute.
document("AndroidManifest.xml").use {
it.getNode("application").attributes.apply {
getNamedItem("android:icon").textContent = "@mipmap/ic_launcher"
val roundIcon = getIcon(firstPixelDensity, ROUND_ICON)
if (roundIcon?.isNotEmpty() == true) {
val roundIconAttribute = getNamedItem("android:roundIcon")
?: setNamedItem(it.createAttribute("android:roundIcon"))
roundIconAttribute.textContent = "@mipmap/ic_launcher_round"
}
}
}
// endregion
// region Change the app icon for each pixel density.
val hasAdaptiveIcon = getIcon(firstPixelDensity, BACKGROUND_ICON)
if (hasAdaptiveIcon?.isNotEmpty() == true) {
val monochromeIconXmlString = if (getIcon(firstPixelDensity, MONOCHROME_ICON)?.isNotEmpty() == true) {
"<monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>"
} else {
""
}
// If an adaptive icon is provided, add the adaptive icon XML file to the res/mipmap-anydpi directory.
get("res/mipmap-anydpi/ic_launcher.xml").writeText(
"""
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
$monochromeIconXmlString
</adaptive-icon>
""".trimIndent(),
)
}
pixelDensities.forEach { pixelDensity ->
val icon = getIcon(pixelDensity, FULL_ICON)!!
// Safe call (?.) is used because the user may just provide the full icon and skip the other optional icons.
val roundIcon = getIcon(pixelDensity, ROUND_ICON)
val backgroundIcon = getIcon(pixelDensity, BACKGROUND_ICON)
val foregroundIcon = getIcon(pixelDensity, FOREGROUND_ICON)
val monochromeIcon = getIcon(pixelDensity, MONOCHROME_ICON)
infix fun String?.to(target: String) {
if (isNullOrEmpty()) {
return
}
Files.copy(
readIcon(this),
get("res/$target").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}
// Copy the icons to the mipmap directory.
icon to "mipmap-$pixelDensity/ic_launcher.png"
roundIcon to "mipmap-$pixelDensity/ic_launcher_round.png"
backgroundIcon to "mipmap-$pixelDensity/ic_launcher_background.png"
foregroundIcon to "mipmap-$pixelDensity/ic_launcher_foreground.png"
monochromeIcon to "drawable-$pixelDensity/ic_launcher_monochrome.png"
}
// endregion
}
if (iconOptions[firstPixelDensity]!!.value === revancedIconOptionValue) {
patchIcon({ pixelDensity, iconIndex ->
when (iconIndex) {
FULL_ICON -> "mipmap-$pixelDensity/revanced-icon"
ROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-round"
BACKGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-background"
FOREGROUND_ICON -> "mipmap-$pixelDensity/revanced-icon-foreground"
MONOCHROME_ICON -> "drawable-$pixelDensity/revanced-icon-monochrome"
else -> throw IllegalArgumentException("Invalid icon index: $iconIndex")
}
}) { icon ->
inputStreamFromBundledResource("change-icon", "$icon.png")!!
}
} else {
patchIcon({ pixelDensity, iconIndex ->
iconOptions[pixelDensity]?.value?.get(iconIndex)
}) { icon ->
get(icon).inputStream()
}
}
}
}

View File

@@ -1,36 +0,0 @@
package app.revanced.patches.all.misc.announcements
import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.*
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
fun announcementsPatch(
mainActivityOnCreateFingerprint: Fingerprint,
extensionPatch: Patch<*>,
extensionClassDescriptor: String,
block: BytecodePatchBuilder.() -> Unit = {},
executeBlock: BytecodePatchContext.() -> Unit = {},
) = bytecodePatch(
name = "Announcements",
description = "Shows announcements from ReVanced on app startup.",
) {
block()
dependsOn(
extensionPatch,
addResourcesPatch,
)
execute {
addResources("shared", "misc.announcements.announcementsPatch")
mainActivityOnCreateFingerprint.method.addInstructions(
0,
"invoke-static/range { p0 .. p0 }, $extensionClassDescriptor->showAnnouncement(Landroid/app/Activity;)V",
)
executeBlock()
}
}

View File

@@ -1,8 +1,8 @@
package app.revanced.patches.music.layout.compactheader
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.AccessFlags
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val constructCategoryBarFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
@@ -15,6 +15,6 @@ internal val constructCategoryBarFingerprint = fingerprint {
Opcode.MOVE_RESULT_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.CONST,
Opcode.INVOKE_VIRTUAL
Opcode.INVOKE_VIRTUAL,
)
}

View File

@@ -1,14 +0,0 @@
package app.revanced.patches.music.misc.announcements
import app.revanced.patches.all.misc.announcements.announcementsPatch
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.shared.musicActivityOnCreateFingerprint
val announcementsPatch = announcementsPatch(
musicActivityOnCreateFingerprint,
sharedExtensionPatch,
"Lapp/revanced/extension/music/announcements/AnnouncementsPatch;",
{
compatibleWith("com.google.android.apps.music")
},
)

View File

@@ -1,4 +1,4 @@
package app.revanced.patches.music.shared
package app.revanced.patches.music.misc.gms
import app.revanced.patcher.fingerprint

View File

@@ -4,7 +4,6 @@ import app.revanced.patcher.patch.Option
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME
import app.revanced.patches.music.shared.musicActivityOnCreateFingerprint
import app.revanced.patches.shared.castContextFetchFingerprint
import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch
import app.revanced.patches.shared.primeMethodFingerprint

View File

@@ -3,4 +3,4 @@ package app.revanced.patches.reddit.customclients.sync.syncforreddit.extension
import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.hooks.initHook
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch("syncforreddit", initHook)
val sharedExtensionPatch = sharedExtensionPatch("sync", initHook)

View File

@@ -5,7 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.twitter.misc.extension.sharedExtensionPatch
import app.revanced.patches.reddit.misc.extension.sharedExtensionPatch
import java.io.InvalidClassException
/**

View File

@@ -1,27 +1,51 @@
package app.revanced.patches.youtube.misc.announcements
import app.revanced.patches.all.misc.announcements.announcementsPatch
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
val announcementsPatch = announcementsPatch(
mainActivityOnCreateFingerprint,
sharedExtensionPatch,
"Lapp/revanced/extension/youtube/patches/announcements/AnnouncementsPatch;",
{
dependsOn(settingsPatch)
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/announcements/AnnouncementsPatch;"
compatibleWith("com.google.android.youtube")
},
{
val announcementsPatch = bytecodePatch(
name = "Announcements",
description = "Adds an option to show announcements from ReVanced on app startup.",
) {
dependsOn(
settingsPatch,
addResourcesPatch,
)
compatibleWith(
"com.google.android.youtube"(
"18.38.44",
"18.49.37",
"19.16.39",
"19.25.37",
"19.34.42",
"19.43.41",
"19.45.38",
"19.46.42",
),
)
execute {
addResources("youtube", "misc.announcements.announcementsPatch")
PreferenceScreen.MISC.addPreferences(
SwitchPreference("revanced_announcements"),
)
},
)
mainActivityOnCreateFingerprint.method.addInstructions(
// Insert index must be greater than the insert index used by GmsCoreSupport,
// as both patch the same method and GmsCore check should be first.
1,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V",
)
}
}

View File

@@ -23,16 +23,14 @@ fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(i
* Returns a sequence for all child nodes.
*/
@Suppress("UNCHECKED_CAST")
fun Node.childElementsSequence() =
this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>
fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence<Element>
/**
* Performs the given [action] on each child element.
*/
inline fun Node.forEachChildElement(action: (Element) -> Unit) =
childElementsSequence().forEach {
action(it)
}
inline fun Node.forEachChildElement(action: (Element) -> Unit) = childElementsSequence().forEach {
action(it)
}
/**
* Recursively traverse the DOM tree starting from the given root node.
@@ -141,7 +139,8 @@ internal fun Node.addResource(
appendChild(resource.serialize(ownerDocument, resourceCallback))
}
internal fun org.w3c.dom.Document.getNode(tagName: String) = this.getElementsByTagName(tagName).item(0)
internal fun org.w3c.dom.Document.getNode(tagName: String) = getElementsByTagName(tagName).item(0)
internal fun Node.getNode(tagName: String) = childNodes.asSequence().find { it.nodeName == tagName }
internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? {
for (i in 0 until length) {
@@ -164,8 +163,7 @@ internal fun NodeList.findElementByAttributeValue(attributeName: String, value:
return null
}
internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) =
findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value")
internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value")
internal fun Element.copyAttributesFrom(oldContainer: Element) {
// Copy attributes from the old element to the new element

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB