feat(Custom branding): Add in-app settings to change icon and name (#6059)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-10-07 22:21:12 +03:00
committed by GitHub
parent 64d22a9c31
commit a50f3b5177
89 changed files with 925 additions and 217 deletions

View File

@@ -31,9 +31,6 @@ import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings("unused")
public class GmsCoreSupport {
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
@@ -53,6 +50,20 @@ public class GmsCoreSupport {
@Nullable
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
private static String getOriginalPackageName() {
return null; // Modified during patching.
}
/**
* @return If the current package name is the same as the original unpatched app.
* If `GmsCore support` was not included during patching, this returns true;
*/
public static boolean isPackageNameOriginal() {
String originalPackageName = getOriginalPackageName();
return originalPackageName == null
|| originalPackageName.equals(Utils.getContext().getPackageName());
}
private static void open(String queryOrLink) {
Logger.printInfo(() -> "Opening link: " + queryOrLink);
@@ -113,11 +124,10 @@ public class GmsCoreSupport {
// Verify the user has not included GmsCore for a root installation.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
String packageName = context.getPackageName();
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
if (isPackageNameOriginal()) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load
// resources from the unpatched app and all patch strings are missing.
// Cannot use localize text here, since the app will load resources
// from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// Do not exit. If the app exits before launch completes (and without
@@ -250,8 +260,8 @@ public class GmsCoreSupport {
};
}
// Modified by a patch. Do not touch.
private static String getGmsCoreVendorGroupId() {
return "app.revanced";
// Modified during patching.
throw new IllegalStateException();
}
}

View File

@@ -0,0 +1,136 @@
package app.revanced.extension.shared.patches;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.shared.GmsCoreSupport;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
/**
* Patch shared by YouTube and YT Music.
*/
@SuppressWarnings("unused")
public class CustomBrandingPatch {
// Important: In the future, additional branding themes can be added but all existing and prior
// themes cannot be removed or renamed.
//
// This is because if a user has a branding theme selected, then only that launch alias is enabled.
// If a future update removes or renames that alias, then after updating the app is effectively
// broken and it cannot be opened and not even clearing the app data will fix it.
// In that situation the only fix is to completely uninstall and reinstall again.
//
// The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
public enum BrandingTheme {
/**
* Original unpatched icon. Must be first enum.
*/
ORIGINAL("revanced_original"),
ROUNDED("revanced_rounded"),
MINIMAL("revanced_minimal"),
SCALED("revanced_scaled"),
/**
* User provided custom icon. Must be the last enum.
*/
CUSTOM("revanced_custom");
public final String themeAlias;
BrandingTheme(String themeAlias) {
this.themeAlias = themeAlias;
}
private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
if (appIndex <= 0) {
throw new IllegalArgumentException("App index starts at index 1");
}
return packageName + '.' + themeAlias + '_' + appIndex;
}
}
/**
* Injection point.
*
* The total number of app name aliases, including dummy aliases.
*/
private static int numberOfPresetAppNames() {
// Modified during patching.
throw new IllegalStateException();
}
/**
* Injection point.
*/
@SuppressWarnings("ConstantConditions")
public static void setBranding() {
try {
if (GmsCoreSupport.isPackageNameOriginal()) {
Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
return;
}
Context context = Utils.getContext();
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
ComponentName componentToEnable = null;
ComponentName defaultComponent = null;
List<ComponentName> componentsToDisable = new ArrayList<>();
for (BrandingTheme theme : BrandingTheme.values()) {
// Must always update all aliases including custom alias (last index).
final int numberOfPresetAppNames = numberOfPresetAppNames();
// App name indices starts at 1.
for (int index = 1; index <= numberOfPresetAppNames; index++) {
String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
ComponentName component = new ComponentName(packageName, aliasClass);
if (defaultComponent == null) {
// Default is always the first alias.
defaultComponent = component;
}
if (index == selectedNameIndex && theme == selectedBranding) {
componentToEnable = component;
} else {
componentsToDisable.add(component);
}
}
}
if (componentToEnable == null) {
// User imported a bad app name index value. Either the imported data
// was corrupted, or they previously had custom name enabled and the app
// no longer has a custom name specified.
Utils.showToastLong("Custom branding reset");
BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
componentToEnable = defaultComponent;
componentsToDisable.remove(defaultComponent);
}
for (ComponentName disable : componentsToDisable) {
// Use info logging because if the alias status become corrupt the app cannot launch.
Logger.printInfo(() -> "Disabling: " + disable.getClassName());
pm.setComponentEnabledSetting(disable,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
ComponentName componentToEnableFinal = componentToEnable;
Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
pm.setComponentEnabledSetting(componentToEnable,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
} catch (Exception ex) {
Logger.printException(() -> "setBranding failure", ex);
}
}
}

View File

@@ -2,6 +2,7 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
@@ -40,4 +41,7 @@ public class BaseSettings {
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
}

View File

@@ -5,6 +5,7 @@ import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
@@ -125,6 +126,8 @@ public class ReVancedAboutPreference extends Preference {
{
setOnPreferenceClickListener(pref -> {
Context context = pref.getContext();
// Show a progress spinner if the social links are not fetched yet.
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
// Show a progress spinner, but only if the api fetch takes more than a half a second.
@@ -137,17 +140,18 @@ public class ReVancedAboutPreference extends Preference {
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
Utils.runOnBackgroundThread(() ->
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress));
} else {
// No network call required and can run now.
fetchLinksAndShowDialog(null, null, null);
fetchLinksAndShowDialog(context, null, null, null);
}
return false;
});
}
private void fetchLinksAndShowDialog(@Nullable Handler handler,
private void fetchLinksAndShowDialog(Context context,
@Nullable Handler handler,
Runnable showDialogRunnable,
@Nullable ProgressDialog progress) {
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
@@ -164,7 +168,17 @@ public class ReVancedAboutPreference extends Preference {
if (handler != null) {
handler.removeCallbacks(showDialogRunnable);
}
if (progress != null) {
// Don't continue if the activity is done. To test this tap the
// about dialog and immediately press back before the dialog can show.
if (context instanceof Activity activity) {
if (activity.isFinishing() || activity.isDestroyed()) {
Logger.printDebug(() -> "Not showing about dialog, activity is closed");
return;
}
}
if (progress != null && progress.isShowing()) {
progress.dismiss();
}
new WebViewDialog(getContext(), htmlDialog).show();

View File

@@ -1919,6 +1919,7 @@ public final class app/revanced/util/ResourceUtilsKt {
public static final fun forEachChildElement (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V
public static final fun insertFirst (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V
public static final fun iterateXmlNodeChildren (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public static final fun removeFromParent (Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
}
public final class app/revanced/util/resource/ArrayResource : app/revanced/util/resource/BaseResource {

View File

@@ -4,6 +4,10 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
import app.revanced.patches.music.misc.gms.musicActivityOnCreateFingerprint
import app.revanced.patches.music.misc.settings.PreferenceScreen
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
import app.revanced.patches.shared.misc.mapping.get
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
@@ -50,24 +54,18 @@ private val disableSplashAnimationPatch = bytecodePatch {
}
}
private const val APP_NAME = "YT Music ReVanced"
@Suppress("unused")
val customBrandingPatch = baseCustomBrandingPatch(
defaultAppName = APP_NAME,
appNameValues = mapOf(
"YT Music ReVanced" to APP_NAME,
"Music ReVanced" to "Music ReVanced",
"Music" to "Music",
"YT Music" to "YT Music",
),
resourceFolder = "custom-branding/music",
iconResourceFileNames = arrayOf(
"adaptiveproduct_youtube_music_2024_q4_background_color_108",
"adaptiveproduct_youtube_music_2024_q4_foreground_color_108",
"ic_launcher_release",
),
monochromeIconFileNames = arrayOf("ic_app_icons_themed_youtube_music.xml"),
addResourcePatchName = "music",
originalLauncherIconName = "ic_launcher_release",
originalAppName = "@string/app_launcher_name",
originalAppPackageName = MUSIC_PACKAGE_NAME,
copyExistingIntentsToAliases = false,
numberOfPresetAppNames = 5,
mainActivityOnCreateFingerprint = musicActivityOnCreateFingerprint,
mainActivityName = MUSIC_MAIN_ACTIVITY_NAME,
activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME,
preferenceScreen = PreferenceScreen.GENERAL,
block = {
dependsOn(disableSplashAnimationPatch)

View File

@@ -1,6 +1,8 @@
package app.revanced.patches.music.misc.gms
object Constants {
internal const val MUSIC_MAIN_ACTIVITY_NAME = "com.google.android.apps.youtube.music.activities.MusicActivity"
internal const val REVANCED_MUSIC_PACKAGE_NAME = "app.revanced.android.apps.youtube.music"
internal const val MUSIC_PACKAGE_NAME = "com.google.android.apps.youtube.music"
}

View File

@@ -1,173 +1,387 @@
package app.revanced.patches.shared.layout.branding
import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.ResourcePatchBuilder
import app.revanced.patcher.patch.ResourcePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import app.revanced.util.findElementByAttributeValueOrThrow
import app.revanced.util.removeFromParent
import app.revanced.util.returnEarly
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.File
import java.nio.file.Files
import java.util.logging.Logger
private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path.
internal val mipmapDirectories = arrayOf(
private val mipmapDirectories = arrayOf(
// Target app does not have ldpi icons.
"mdpi",
"hdpi",
"xhdpi",
"xxhdpi",
"xxxhdpi",
).map { "mipmap-$it" }.toTypedArray()
"mipmap-mdpi",
"mipmap-hdpi",
"mipmap-xhdpi",
"mipmap-xxhdpi",
"mipmap-xxxhdpi"
)
private fun formatResourceFileList(resourceNames: Array<String>) = resourceNames.joinToString("\n") { "- $it" }
private val iconStyleNames = arrayOf(
"rounded",
"minimal",
"scaled"
)
/**
* Attempts to fix unescaped and invalid characters not allowed for an Android app name.
*/
private fun escapeAppName(name: String): String? {
// Remove ASCII control characters.
val cleanedName = name.filter { it.code >= 32 }
private const val ORIGINAL_USER_ICON_STYLE_NAME = "original"
private const val CUSTOM_USER_ICON_STYLE_NAME = "custom"
// Replace invalid XML characters with escaped equivalents.
val escapedName = cleanedName
.replace("&", "&amp;") // Must be first to avoid double-escaping.
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(Regex("(?<!&)\""), "&quot;")
private const val LAUNCHER_RESOURCE_NAME_PREFIX = "revanced_launcher_"
private const val LAUNCHER_ADAPTIVE_BACKGROUND_PREFIX = "revanced_adaptive_background_"
private const val LAUNCHER_ADAPTIVE_FOREGROUND_PREFIX = "revanced_adaptive_foreground_"
private const val LAUNCHER_ADAPTIVE_MONOCHROME_PREFIX = "revanced_adaptive_monochrome_"
// Trim empty spacing.
val trimmed = escapedName.trim()
private val USER_CUSTOM_ADAPTIVE_FILE_NAMES = arrayOf(
"$LAUNCHER_ADAPTIVE_BACKGROUND_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.png",
"$LAUNCHER_ADAPTIVE_FOREGROUND_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.png"
)
return trimmed.ifBlank { null }
}
private const val USER_CUSTOM_MONOCHROME_NAME = "$LAUNCHER_ADAPTIVE_MONOCHROME_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.xml"
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/patches/CustomBrandingPatch;"
/**
* Shared custom branding patch for YouTube and YT Music.
*/
internal fun baseCustomBrandingPatch(
defaultAppName: String,
appNameValues: Map<String, String>,
resourceFolder: String,
iconResourceFileNames: Array<String>,
monochromeIconFileNames: Array<String>,
block: ResourcePatchBuilder.() -> Unit = {},
addResourcePatchName: String,
originalLauncherIconName: String,
originalAppName: String,
originalAppPackageName: String,
copyExistingIntentsToAliases: Boolean,
numberOfPresetAppNames: Int,
mainActivityOnCreateFingerprint: Fingerprint,
mainActivityName: String,
activityAliasNameWithIntents: String,
preferenceScreen: BasePreferenceScreen.Screen,
block: ResourcePatchBuilder.() -> Unit,
executeBlock: ResourcePatchContext.() -> Unit = {}
): ResourcePatch = resourcePatch(
name = "Custom branding",
description = "Applies a custom app name and icon. Defaults to \"$defaultAppName\" and the ReVanced logo.",
use = false,
description = "Adds options to change the app icon and app name. " +
"Branding cannot be changed for mounted (root) installations."
) {
val iconResourceFileNamesPng = iconResourceFileNames.map { "$it.png" }.toTypedArray<String>()
val appName by stringOption(
key = "appName",
default = defaultAppName,
values = appNameValues,
val customName by stringOption(
key = "customName",
title = "App name",
description = "The name of the app.",
description = "Custom app name."
)
val iconPath by stringOption(
key = "iconPath",
default = REVANCED_ICON,
values = mapOf("ReVanced Logo" to REVANCED_ICON),
title = "App icon",
val customIcon by stringOption(
key = "customIcon",
title = "Custom icon",
description = """
The icon to apply to the app.
Folder with images to use as a custom icon.
If a path to a folder is provided, the folder must contain the following folders:
${formatResourceFileList(mipmapDirectories)}
Each of these folders must contain the following files:
${formatResourceFileList(iconResourceFileNamesPng)}
The folder must contain one or more of the following folders, depending on the DPI of the device:
${mipmapDirectories.joinToString("\n") { "- $it" }}
Optionally, a 'drawable' folder with the monochrome icon files:
${formatResourceFileList(monochromeIconFileNames)}
""".trimIndentMultiline(),
Each of the folders must contain all of the following files:
${USER_CUSTOM_ADAPTIVE_FILE_NAMES.joinToString("\n")}
Optionally, the path can contain a 'drawable' folder with the monochrome icon file:
$USER_CUSTOM_MONOCHROME_NAME
""".trimIndentMultiline()
)
block()
dependsOn(
addResourcesPatch,
bytecodePatch {
execute {
mainActivityOnCreateFingerprint.method.addInstruction(
0,
"invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->setBranding()V"
)
numberOfPresetAppNamesExtensionFingerprint.method.returnEarly(numberOfPresetAppNames)
}
}
)
finalize {
val useCustomName = customName != null
val useCustomIcon = customIcon != null
if (setOrGetFallbackPackageName(originalAppPackageName) == originalAppPackageName) {
if (useCustomName || useCustomIcon) {
Logger.getLogger(this::class.java.name).warning(
"Custom branding does not work with root installation. No changes applied."
)
}
return@finalize
}
preferenceScreen.addPreferences(
if (useCustomName) {
ListPreference(
key = "revanced_custom_branding_name",
entriesKey = "revanced_custom_branding_name_custom_entries",
entryValuesKey = "revanced_custom_branding_name_custom_entry_values"
)
} else {
ListPreference("revanced_custom_branding_name")
},
if (useCustomIcon) {
ListPreference(
key = "revanced_custom_branding_icon",
entriesKey = "revanced_custom_branding_icon_custom_entries",
entryValuesKey = "revanced_custom_branding_icon_custom_entry_values"
)
} else {
ListPreference("revanced_custom_branding_icon")
}
)
}
execute {
val mipmapIconResourceGroups = mipmapDirectories.map { directory ->
ResourceGroup(
directory,
*iconResourceFileNamesPng,
addResources("shared", "layout.branding.baseCustomBrandingPatch")
addResources(addResourcePatchName, "layout.branding.customBrandingPatch")
val useCustomName = customName != null
val useCustomIcon = customIcon != null
iconStyleNames.forEach { style ->
copyResources(
"custom-branding",
ResourceGroup(
"drawable",
"$LAUNCHER_ADAPTIVE_BACKGROUND_PREFIX$style.xml",
"$LAUNCHER_ADAPTIVE_FOREGROUND_PREFIX$style.xml",
"$LAUNCHER_ADAPTIVE_MONOCHROME_PREFIX$style.xml"
),
ResourceGroup(
"mipmap-anydpi",
"$LAUNCHER_RESOURCE_NAME_PREFIX$style.xml"
)
)
}
val iconPathTrimmed = iconPath!!.trim()
if (iconPathTrimmed == REVANCED_ICON) {
// Replace mipmap icons with preset patch icons.
mipmapIconResourceGroups.forEach { groupResources ->
copyResources(resourceFolder, groupResources)
}
// Copy template user icon, because the aliases must be added even if no user icon is provided.
copyResources(
"custom-branding",
ResourceGroup(
"mipmap-anydpi",
"$LAUNCHER_RESOURCE_NAME_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.xml",
),
ResourceGroup(
"drawable",
"$LAUNCHER_ADAPTIVE_MONOCHROME_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.xml",
)
)
// Replace monochrome icons.
monochromeIconFileNames.forEach { fileName ->
copyResources(
resourceFolder,
ResourceGroup("drawable", fileName)
// Copy template icon png files.
mipmapDirectories.forEach { dpi ->
copyResources(
"custom-branding",
ResourceGroup(
dpi,
"$LAUNCHER_ADAPTIVE_BACKGROUND_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.png",
"$LAUNCHER_ADAPTIVE_FOREGROUND_PREFIX$CUSTOM_USER_ICON_STYLE_NAME.png",
)
)
}
if (useCustomIcon) {
// Copy user provided files
val iconPathFile = File(customIcon!!.trim())
if (!iconPathFile.exists()) {
throw PatchException(
"The custom icon path cannot be found: " + iconPathFile.absolutePath
)
}
} else {
val filePath = File(iconPathTrimmed)
if (!iconPathFile.isDirectory) {
throw PatchException(
"The custom icon path must be a folder: " + iconPathFile.absolutePath
)
}
val sourceFolders = iconPathFile.listFiles { file -> file.isDirectory }
?: throw PatchException("The custom icon path contains no subfolders: " +
iconPathFile.absolutePath)
val resourceDirectory = get("res")
var copiedFiles = false
// Replace
mipmapIconResourceGroups.forEach { groupResources ->
val groupResourceDirectoryName = groupResources.resourceDirectoryName
val fromDirectory = filePath.resolve(groupResourceDirectoryName)
val toDirectory = resourceDirectory.resolve(groupResourceDirectoryName)
// For each source folder, copy the files to the target resource directories.
sourceFolders.forEach { dpiSourceFolder ->
val targetDpiFolder = resourceDirectory.resolve(dpiSourceFolder.name)
if (!targetDpiFolder.exists()) return@forEach
groupResources.resources.forEach { iconFileName ->
Files.write(
toDirectory.resolve(iconFileName).toPath(),
fromDirectory.resolve(iconFileName).readBytes(),
)
val customFiles = dpiSourceFolder.listFiles { file ->
file.isFile && file.name in USER_CUSTOM_ADAPTIVE_FILE_NAMES
}!!
if (customFiles.size > 0 && customFiles.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) {
throw PatchException("Must include all required icon files " +
"but only found " + customFiles.map { it.name })
}
customFiles.forEach { imgSourceFile ->
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
imgSourceFile.copyTo(target = imgTargetFile, overwrite = true)
copiedFiles = true
}
}
// Copy all monochrome icons if provided.
monochromeIconFileNames.forEach { fileName ->
val replacementMonochrome = filePath.resolve("drawable").resolve(fileName)
if (replacementMonochrome.exists()) {
Files.write(
resourceDirectory.resolve("drawable").resolve(fileName).toPath(),
replacementMonochrome.readBytes(),
)
}
// Copy monochrome if it provided.
val monochromeRelativePath = "drawable/$USER_CUSTOM_MONOCHROME_NAME"
val monochromeFile = iconPathFile.resolve(monochromeRelativePath)
if (monochromeFile.exists()) {
monochromeFile.copyTo(
target = resourceDirectory.resolve(monochromeRelativePath),
overwrite = true
)
copiedFiles = true
}
if (!copiedFiles) {
throw PatchException("Could not find any replacement images in " +
"patch option path: " + iconPathFile.absolutePath)
}
}
// Change the app name.
escapeAppName(appName!!)?.let { escapedAppName ->
val newValue = "android:label=\"$escapedAppName\""
document("AndroidManifest.xml").use { document ->
// Create launch aliases that can be programmatically selected in app.
fun createAlias(
aliasName: String,
iconMipmapName: String,
appNameIndex: Int,
useCustomName: Boolean,
enabled: Boolean,
intents: NodeList
): Element {
val label = if (useCustomName) {
if (customName == null) {
"Custom" // Dummy text, and normally cannot be seen.
} else {
customName!!
}
} else if (appNameIndex == 1) {
// Indexing starts at 1.
originalAppName
} else {
"@string/revanced_custom_branding_name_entry_$appNameIndex"
}
val alias = document.createElement("activity-alias")
alias.setAttribute("android:name", aliasName)
alias.setAttribute("android:enabled", enabled.toString())
alias.setAttribute("android:exported", "true")
alias.setAttribute("android:icon", "@mipmap/$iconMipmapName")
alias.setAttribute("android:label", label)
alias.setAttribute("android:targetActivity", mainActivityName)
val manifest = get("AndroidManifest.xml")
val original = manifest.readText()
val replacement = original
// YouTube
.replace("android:label=\"@string/application_name\"", newValue)
// YT Music
.replace("android:label=\"@string/app_launcher_name\"", newValue)
// Copy all intents from the original alias so long press actions still work.
if (copyExistingIntentsToAliases) {
for (i in 0 until intents.length) {
alias.appendChild(
intents.item(i).cloneNode(true)
)
}
} else {
val intentFilter = document.createElement("intent-filter").apply {
val action = document.createElement("action")
action.setAttribute("android:name", "android.intent.action.MAIN")
appendChild(action)
if (original == replacement) {
Logger.getLogger(this::class.java.name).warning(
"Could not replace manifest app name"
val category = document.createElement("category")
category.setAttribute("android:name", "android.intent.category.LAUNCHER")
appendChild(category)
}
alias.appendChild(intentFilter)
}
return alias
}
val intentFilters = document.childNodes.findElementByAttributeValueOrThrow(
"android:name",
activityAliasNameWithIntents
).childNodes
val application = document.getElementsByTagName("application").item(0) as Element
for (appNameIndex in 1 .. numberOfPresetAppNames) {
fun aliasName(name: String): String = ".revanced_" + name + '_' + appNameIndex
val useCustomNameLabel = (useCustomName && appNameIndex == numberOfPresetAppNames)
// Original icon.
application.appendChild(
createAlias(
aliasName = aliasName(ORIGINAL_USER_ICON_STYLE_NAME),
iconMipmapName = originalLauncherIconName,
appNameIndex = appNameIndex,
useCustomName = useCustomNameLabel,
enabled = (appNameIndex == 1),
intentFilters
)
)
// Bundled icons.
iconStyleNames.forEachIndexed { index, style ->
application.appendChild(
createAlias(
aliasName = aliasName(style),
iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + style,
appNameIndex = appNameIndex,
useCustomName = useCustomNameLabel,
enabled = false,
intentFilters
)
)
}
// User provided custom icon.
//
// Must add all aliases even if the user did not provide a custom icon of their own.
// This is because if the user installs with an option, then repatches without the option,
// the alias must still exist because if it was previously enabled and then it's removed
// the app will become broken and cannot launch. Even if the app data is cleared
// it still cannot be launched and the only fix is to uninstall the app.
// To prevent this, always include all aliases and use dummy data if needed.
application.appendChild(
createAlias(
aliasName = aliasName(CUSTOM_USER_ICON_STYLE_NAME),
iconMipmapName = LAUNCHER_RESOURCE_NAME_PREFIX + CUSTOM_USER_ICON_STYLE_NAME,
appNameIndex = appNameIndex,
useCustomName = useCustomNameLabel,
enabled = false,
intentFilters
)
)
}
manifest.writeText(replacement)
// Remove the main action from the original alias, otherwise two apps icons
// can be shown in the launcher. Can only be done after adding the new aliases.
intentFilters.findElementByAttributeValueOrThrow(
"android:name",
"android.intent.action.MAIN"
).removeFromParent()
}
executeBlock() // Must be after the main code to rename the new icons for YouTube 19.34+.
executeBlock()
}
}

View File

@@ -0,0 +1,13 @@
package app.revanced.patches.shared.layout.branding
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal val numberOfPresetAppNamesExtensionFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("I")
parameters()
custom { method, classDef ->
method.name == "numberOfPresetAppNames" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}

View File

@@ -79,7 +79,7 @@ internal val darkThemeBackgroundColorOption = stringOption(
*/
internal fun baseThemePatch(
extensionClassDescriptor: String,
block: BytecodePatchBuilder.() -> Unit = {},
block: BytecodePatchBuilder.() -> Unit,
executeBlock: BytecodePatchContext.() -> Unit = {}
) = bytecodePatch(
name = "Theme",

View File

@@ -1,16 +1,11 @@
package app.revanced.patches.shared.misc.gms
import app.revanced.patcher.fingerprint
import app.revanced.patches.shared.misc.gms.EXTENSION_CLASS_DESCRIPTOR
import com.android.tools.smali.dexlib2.AccessFlags
const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId"
internal val gmsCoreSupportFingerprint = fingerprint {
custom { _, classDef ->
classDef.endsWith("GmsCoreSupport;")
}
}
internal val googlePlayUtilityFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("I")
@@ -28,3 +23,21 @@ internal val serviceCheckFingerprint = fingerprint {
parameters("L", "I")
strings("Google Play Services not available")
}
internal val gmsCoreSupportFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("Ljava/lang/String;")
parameters()
custom { method, classDef ->
method.name == "getGmsCoreVendorGroupId" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}
internal val originalPackageNameExtensionFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("Ljava/lang/String;")
parameters()
custom { methodDef, classDef ->
methodDef.name == "getOriginalPackageName" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}

View File

@@ -1,10 +1,18 @@
package app.revanced.patches.shared.misc.gms
import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Option
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatchBuilder
import app.revanced.patcher.patch.ResourcePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.all.misc.packagename.changePackageNamePatch
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
import app.revanced.patches.all.misc.resources.addResources
@@ -12,7 +20,8 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.gms.Constants.ACTIONS
import app.revanced.patches.shared.misc.gms.Constants.AUTHORITIES
import app.revanced.patches.shared.misc.gms.Constants.PERMISSIONS
import app.revanced.util.*
import app.revanced.util.getReference
import app.revanced.util.returnEarly
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@@ -23,6 +32,8 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
import org.w3c.dom.Element
import org.w3c.dom.Node
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/GmsCoreSupport;"
private const val PACKAGE_NAME_REGEX_PATTERN = "^[a-z]\\w*(\\.[a-z]\\w*)+\$"
/**
@@ -201,19 +212,18 @@ fun gmsCoreSupportPatch(
googlePlayUtilityFingerprint.method.returnEarly(0)
}
// Set original and patched package names for extension to use.
originalPackageNameExtensionFingerprint.method.returnEarly(fromPackageName)
// Verify GmsCore is installed and whitelisted for power optimizations and background usage.
mainActivityOnCreateFingerprint.method.apply {
addInstructions(
0,
"invoke-static/range { p0 .. p0 }, Lapp/revanced/extension/shared/GmsCoreSupport;->" +
"checkGmsCore(Landroid/app/Activity;)V",
)
}
mainActivityOnCreateFingerprint.method.addInstruction(
0,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" +
"checkGmsCore(Landroid/app/Activity;)V"
)
// Change the vendor of GmsCore in the extension.
gmsCoreSupportFingerprint.classDef.methods
.single { it.name == GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME }
.replaceInstruction(0, "const-string v0, \"$gmsCoreVendorGroupId\"")
gmsCoreSupportFingerprint.method.returnEarly(gmsCoreVendorGroupId!!)
executeBlock()
}

View File

@@ -43,7 +43,7 @@ internal fun spoofVideoStreamsPatch(
fixMediaFetchHotConfig: BytecodePatchBuilder.() -> Boolean = { false },
fixMediaFetchHotConfigAlternative: BytecodePatchBuilder.() -> Boolean = { false },
fixParsePlaybackResponseFeatureFlag: BytecodePatchBuilder.() -> Boolean = { false },
block: BytecodePatchBuilder.() -> Unit = {},
block: BytecodePatchBuilder.() -> Unit,
executeBlock: BytecodePatchContext.() -> Unit = {},
) = bytecodePatch(
name = "Spoof video streams",

View File

@@ -1,38 +1,28 @@
package app.revanced.patches.youtube.layout.branding
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
import app.revanced.patches.shared.layout.branding.mipmapDirectories
import java.nio.file.Files
private const val APP_NAME = "YouTube ReVanced"
private val youtubeIconResourceFileNames_19_34 = mapOf(
"adaptiveproduct_youtube_foreground_color_108" to "adaptiveproduct_youtube_2024_q4_foreground_color_108",
"adaptiveproduct_youtube_background_color_108" to "adaptiveproduct_youtube_2024_q4_background_color_108",
)
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.gms.Constants.YOUTUBE_MAIN_ACTIVITY_NAME
import app.revanced.patches.youtube.misc.gms.Constants.YOUTUBE_PACKAGE_NAME
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
@Suppress("unused")
val customBrandingPatch = baseCustomBrandingPatch(
defaultAppName = APP_NAME,
appNameValues = mapOf(
"YouTube ReVanced" to APP_NAME,
"YT ReVanced" to "YT ReVanced",
"YT" to "YT",
"YouTube" to "YouTube",
),
resourceFolder = "custom-branding/youtube",
iconResourceFileNames = arrayOf(
"adaptiveproduct_youtube_background_color_108",
"adaptiveproduct_youtube_foreground_color_108",
"ic_launcher",
"ic_launcher_round",
),
monochromeIconFileNames = arrayOf(
"adaptive_monochrome_ic_youtube_launcher.xml",
"ringo2_adaptive_monochrome_ic_youtube_launcher.xml"
),
addResourcePatchName = "youtube",
originalLauncherIconName = "ic_launcher",
originalAppName = "@string/application_name",
originalAppPackageName = YOUTUBE_PACKAGE_NAME,
copyExistingIntentsToAliases = true,
numberOfPresetAppNames = 5,
mainActivityOnCreateFingerprint = mainActivityOnCreateFingerprint,
mainActivityName = YOUTUBE_MAIN_ACTIVITY_NAME,
activityAliasNameWithIntents = "com.google.android.youtube.app.honeycomb.Shell\$HomeActivity",
preferenceScreen = PreferenceScreen.GENERAL_LAYOUT,
block = {
dependsOn(sharedExtensionPatch)
compatibleWith(
"com.google.android.youtube"(
"19.34.42",
@@ -41,20 +31,5 @@ val customBrandingPatch = baseCustomBrandingPatch(
"20.14.43",
)
)
},
executeBlock = {
val resourceDirectory = get("res")
mipmapDirectories.forEach { directory ->
val targetDirectory = resourceDirectory.resolve(directory)
youtubeIconResourceFileNames_19_34.forEach { (old, new) ->
val oldFile = targetDirectory.resolve("$old.png")
val newFile = targetDirectory.resolve("$new.png")
Files.write(newFile.toPath(), oldFile.readBytes())
}
}
}
)

View File

@@ -136,8 +136,21 @@ val changeHeaderPatch = resourcePatch(
)
if (custom != null) {
val sourceFolders = File(custom!!).listFiles { file -> file.isDirectory }
?: throw PatchException("The provided path is not a directory: $custom")
val customFile = File(custom!!)
if (!customFile.exists()) {
throw PatchException("The custom icon path cannot be found: " +
customFile.absolutePath
)
}
if (!customFile.isDirectory) {
throw PatchException("The custom icon path must be a folder: "
+ customFile.absolutePath)
}
val sourceFolders = customFile.listFiles { file -> file.isDirectory }
?: throw PatchException("The custom icon path contains no subfolders: " +
customFile.absolutePath)
val customResourceFileNames = getLightDarkFileNames(CUSTOM_HEADER_RESOURCE_NAME)
@@ -166,7 +179,8 @@ val changeHeaderPatch = resourcePatch(
}
if (!copiedFiles) {
throw PatchException("No custom header images found in the provided path: $custom")
throw PatchException("No custom header images found in " +
"the provided path: " + customFile.absolutePath)
}
}

View File

@@ -27,6 +27,7 @@ import app.revanced.util.forEachLiteralValueInstruction
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstruction
import app.revanced.util.removeFromParent
import app.revanced.util.returnLate
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@@ -127,7 +128,7 @@ private val hideShortsComponentsResourcePatch = resourcePatch {
)
if (hideShortsAppShortcut == true) {
shortsItem.parentNode.removeChild(shortsItem)
shortsItem.removeFromParent()
}
}
@@ -138,7 +139,7 @@ private val hideShortsComponentsResourcePatch = resourcePatch {
)
if (hideShortsWidget == true) {
shortsItem.parentNode.removeChild(shortsItem)
shortsItem.removeFromParent()
}
}

View File

@@ -1,6 +1,8 @@
package app.revanced.patches.youtube.misc.gms
internal object Constants {
internal const val YOUTUBE_MAIN_ACTIVITY_NAME = "com.google.android.apps.youtube.app.watchwhile.MainActivity"
const val YOUTUBE_PACKAGE_NAME = "com.google.android.youtube"
const val REVANCED_YOUTUBE_PACKAGE_NAME = "app.revanced.android.youtube"
}

View File

@@ -14,6 +14,13 @@ import java.nio.file.StandardCopyOption
private val classLoader = object {}.javaClass.classLoader
/**
* Removes a node from its parent.
*
* @return The node that was removed (object this method was called on).
*/
fun Node.removeFromParent() : Node = parentNode.removeChild(this)
/**
* Returns a sequence for all child nodes.
*/
@@ -70,8 +77,13 @@ fun ResourcePatchContext.copyResources(
for (resourceGroup in resources) {
resourceGroup.resources.forEach { resource ->
val resourceFile = "${resourceGroup.resourceDirectoryName}/$resource"
val stream = inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)
if (stream == null) {
throw IllegalArgumentException("Could not find resource: $resourceFile " +
"in directory: $sourceResourceDirectory")
}
Files.copy(
inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!!,
stream,
targetResourceDirectory.resolve(resourceFile).toPath(),
StandardCopyOption.REPLACE_EXISTING,
)

View File

@@ -1,5 +1,33 @@
<resources>
<app id="shared">
<patch id="layout.branding.baseCustomBrandingPatch">
<string-array name="revanced_custom_branding_icon_entries">
<item>@string/revanced_custom_branding_icon_entry_1</item>
<item>@string/revanced_custom_branding_icon_entry_2</item>
<item>@string/revanced_custom_branding_icon_entry_3</item>
<item>@string/revanced_custom_branding_icon_entry_4</item>
</string-array>
<string-array name="revanced_custom_branding_icon_entry_values">
<item>ORIGINAL</item>
<item>ROUNDED</item>
<item>MINIMAL</item>
<item>SCALED</item>
</string-array>
<string-array name="revanced_custom_branding_icon_custom_entries">
<item>@string/revanced_custom_branding_icon_entry_1</item>
<item>@string/revanced_custom_branding_icon_entry_2</item>
<item>@string/revanced_custom_branding_icon_entry_3</item>
<item>@string/revanced_custom_branding_icon_entry_4</item>
<item>@string/revanced_custom_branding_icon_entry_5</item>
</string-array>
<string-array name="revanced_custom_branding_icon_custom_entry_values">
<item>ORIGINAL</item>
<item>ROUNDED</item>
<item>MINIMAL</item>
<item>SCALED</item>
<item>CUSTOM</item>
</string-array>
</patch>
<patch id="misc.settings.settingsResourcePatch">
<string-array name="revanced_language_entries">
<item>@string/revanced_language_DEFAULT</item>
@@ -123,6 +151,36 @@
</patch>
</app>
<app id="music">
<patch id="layout.branding.customBrandingPatch">
<string-array name="revanced_custom_branding_name_entries">
<!-- Original must be first. -->
<item>@string/app_launcher_name</item>
<item>@string/revanced_custom_branding_name_entry_2</item>
<item>@string/revanced_custom_branding_name_entry_3</item>
<item>@string/revanced_custom_branding_name_entry_4</item>
</string-array>
<string-array name="revanced_custom_branding_name_entry_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
<string-array name="revanced_custom_branding_name_custom_entries">
<!-- Original must be first. -->
<item>@string/app_launcher_name</item>
<item>@string/revanced_custom_branding_name_entry_2</item>
<item>@string/revanced_custom_branding_name_entry_3</item>
<item>@string/revanced_custom_branding_name_entry_4</item>
<item>@string/revanced_custom_branding_name_entry_5</item>
</string-array>
<string-array name="revanced_custom_branding_name_custom_entry_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
</string-array>
</patch>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android VR</item>
@@ -135,6 +193,36 @@
</patch>
</app>
<app id="youtube">
<patch id="layout.branding.customBrandingPatch">
<string-array name="revanced_custom_branding_name_entries">
<!-- Original must be first. -->
<item>@string/application_name</item>
<item>@string/revanced_custom_branding_name_entry_2</item>
<item>@string/revanced_custom_branding_name_entry_3</item>
<item>@string/revanced_custom_branding_name_entry_4</item>
</string-array>
<string-array name="revanced_custom_branding_name_entry_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
<string-array name="revanced_custom_branding_name_custom_entries">
<!-- Original must be first. -->
<item>@string/application_name</item>
<item>@string/revanced_custom_branding_name_entry_2</item>
<item>@string/revanced_custom_branding_name_entry_3</item>
<item>@string/revanced_custom_branding_name_entry_4</item>
<item>@string/revanced_custom_branding_name_entry_5</item>
</string-array>
<string-array name="revanced_custom_branding_name_custom_entry_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
</string-array>
</patch>
<patch id="misc.fix.playback.spoofVideoStreamsPatch">
<string-array name="revanced_spoof_video_streams_client_type_entries">
<item>Android VR</item>

View File

@@ -19,6 +19,19 @@ Second \"item\" text"</string>
-->
<resources>
<app id="shared">
<patch id="layout.branding.baseCustomBrandingPatch">
<string name="revanced_custom_branding_name_title">App name</string>
<!-- Translations of this should be identical to revanced_custom_branding_icon_entry_5 -->
<string name="revanced_custom_branding_name_entry_5">Custom</string>
<string name="revanced_custom_branding_icon_title">App icon</string>
<string name="revanced_custom_branding_icon_entry_1">Original</string>
<string name="revanced_custom_branding_icon_entry_2">ReVanced</string>
<!-- Translation of this should be identical to revanced_header_logo_entry_5 -->
<string name="revanced_custom_branding_icon_entry_3">ReVanced minimal</string>
<string name="revanced_custom_branding_icon_entry_4">ReVanced scaled</string>
<!-- Translations of this should be identical to revanced_custom_branding_name_entry_5 -->
<string name="revanced_custom_branding_icon_entry_5">Custom</string>
</patch>
<patch id="misc.checks.checkEnvironmentPatch">
<string name="revanced_check_environment_failed_title">Checks failed</string>
<string name="revanced_check_environment_dialog_open_official_source_button">Open official website</string>
@@ -1484,13 +1497,18 @@ Swipe to expand or close"</string>
<string name="revanced_seekbar_custom_color_accent_summary">The accent color of the seekbar</string>
<string name="revanced_seekbar_custom_color_invalid">Invalid seekbar color value</string>
</patch>
<patch id="layout.branding.customBrandingPatch">
<string name="revanced_custom_branding_name_entry_2" translatable="false">YouTube ReVanced</string>
<string name="revanced_custom_branding_name_entry_3" translatable="false">YT ReVanced</string>
<string name="revanced_custom_branding_name_entry_4" translatable="false">YT</string>
</patch>
<patch id="layout.branding.changeHeaderPatch">
<string name="revanced_header_logo_title">Header logo</string>
<string name="revanced_header_logo_entry_1">Default</string>
<string name="revanced_header_logo_entry_2">Regular</string>
<string name="revanced_header_logo_entry_3" translatable="false">Premium</string>
<string name="revanced_header_logo_entry_4" translatable="false">ReVanced</string>
<!-- For this situation "Minimal" means minimalistic. It does not mean small or tiny. -->
<!-- Translation of this should be identical to revanced_custom_branding_icon_entry_3 -->
<string name="revanced_header_logo_entry_5">ReVanced minimal</string>
<string name="revanced_header_logo_entry_6">Custom</string>
</patch>
@@ -1708,6 +1726,11 @@ Video playback with AV1 may stutter or drop frames."</string>
</patch>
</app>
<app id="music">
<patch id="layout.branding.customBrandingPatch">
<string name="revanced_custom_branding_name_entry_2" translatable="false">YT Music ReVanced</string>
<string name="revanced_custom_branding_name_entry_3" translatable="false">Music ReVanced</string>
<string name="revanced_custom_branding_name_entry_4" translatable="false">Music</string>
</patch>
<patch id="misc.settings.settingsPatch">
<string name="revanced_settings_music_screen_0_about_title">About</string>
<string name="revanced_settings_music_screen_1_ads_title">Ads</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,2 @@
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#1B1B1B" />

View File

@@ -0,0 +1,2 @@
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#1B1B1B" />

View File

@@ -0,0 +1,2 @@
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#000000" />

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256">
<!-- Android launcher can hide the background layer if it's black or nearly black.
The dark background is needed to see the V shape in day mode. To prevent hiding
the layer it must be added to the foreground. -->
<path
android:pathData="M0,0 L256,0 L256,256 L0,256 Z"
android:fillColor="#1B1B1B" />
<group android:scaleX="0.24"
android:scaleY="0.24"
android:translateX="97.28"
android:translateY="97.28">
<path
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0L228.81,0C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0L14.73,0C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256L137.55,256C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,-0 63.62,-0L192.37,-0C195.32,-0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="128"
android:startY="-0"
android:endX="128"
android:endY="254.6"
android:type="linear">
<item android:offset="0" android:color="#FFF04E98"/>
<item android:offset="0.5" android:color="#FF5F65D4"/>
<item android:offset="1" android:color="#FF4E98F0"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@@ -0,0 +1,50 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="800"
android:viewportHeight="800">
<group android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="200"
android:translateY="200">
<path
android:pathData="M400,400m-400,0a400,400 0,1 1,800 0a400,400 0,1 1,-800 0"
android:fillColor="#1B1B1B"/>
<path
android:pathData="M400,0c220.77,0 400,179.23 400,400c-0,220.77 -179.23,400 -400,400c-220.77,-0 -400,-179.23 -400,-400c0,-220.77 179.23,-400 400,-400ZM400,36c200.9,-0 364,163.1 364,364c0,200.9 -163.1,364 -364,364c-200.9,0 -364,-163.1 -364,-364c-0,-200.9 163.1,-364 364,-364Z"
android:strokeLineJoin="round"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="400"
android:startY="0"
android:endX="400"
android:endY="800"
android:type="linear">
<item android:offset="0" android:color="#FFF04E98"/>
<item android:offset="0.5" android:color="#FF5F65D4"/>
<item android:offset="1" android:color="#FF4E98F0"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37c-2.02,-3.09 -5.46,-4.95 -9.16,-4.95c-5.15,0 -10.44,0 -14.16,0c-3.1,0 -5.91,1.83 -7.15,4.67c-12.47,28.4 -78.27,178.27 -100.25,228.33c-1.25,2.84 -4.05,4.67 -7.15,4.67c-3.1,0 -5.91,-1.83 -7.15,-4.67c-21.98,-50.06 -87.78,-199.93 -100.25,-228.33c-1.25,-2.84 -4.05,-4.67 -7.15,-4.67c-3.73,0 -9.02,0 -14.16,0c-3.69,0 -7.14,1.86 -9.16,4.95c-2.02,3.09 -2.34,6.99 -0.86,10.37c23.56,53.77 101.87,232.52 117.87,269.03c1.74,3.98 5.67,6.55 10.02,6.55c6.29,-0 15.41,-0 21.7,-0c4.34,-0 8.27,-2.57 10.02,-6.55c16,-36.51 94.32,-215.27 117.87,-269.03Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M408.12,395.31c-1.67,2.9 -4.77,4.69 -8.12,4.69c-3.35,-0 -6.44,-1.79 -8.12,-4.69c-17,-29.44 -56.16,-97.26 -73.15,-126.7c-1.67,-2.9 -1.67,-6.47 0,-9.38c1.67,-2.9 4.77,-4.69 8.12,-4.69c33.99,0 112.31,0 146.31,0c3.35,0 6.44,1.79 8.12,4.69c1.67,2.9 1.67,6.47 -0,9.38c-17,29.44 -56.16,97.26 -73.15,126.7Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="400"
android:startY="254.54"
android:endX="400"
android:endY="543.86"
android:type="linear">
<item android:offset="0" android:color="#FFF04E98"/>
<item android:offset="0.5" android:color="#FF5F65D4"/>
<item android:offset="1" android:color="#FF4E98F0"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256">
<!-- Android launcher can hide the background layer if it's black or nearly black.
The dark background is needed to see the V shape in day mode. To prevent hiding
the layer it must be added to the foreground. -->
<path
android:pathData="M0,0 L256,0 L256,256 L0,256 Z"
android:fillColor="#000000" />
<group android:scaleX="0.3"
android:scaleY="0.3"
android:translateX="89.6"
android:translateY="89.6">
<path
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0L228.81,0C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0L14.73,0C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256L137.55,256C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M135.14,123.87C133.67,126.43 130.94,128 128,128C125.05,128 122.33,126.43 120.85,123.87C105.89,97.97 71.44,38.28 56.48,12.37C55,9.82 55,6.68 56.48,4.12C57.95,1.57 60.68,-0 63.62,-0L192.37,-0C195.32,-0 198.04,1.57 199.52,4.12C200.99,6.68 200.99,9.82 199.52,12.37C184.56,38.28 150.1,97.97 135.14,123.87Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="128"
android:startY="0"
android:endX="128"
android:endY="254.6"
android:type="linear">
<item android:offset="0" android:color="#FFF04E98"/>
<item android:offset="0.5" android:color="#FF5F65D4"/>
<item android:offset="1" android:color="#FF4E98F0"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View File

@@ -3,10 +3,10 @@
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256">
<group android:scaleX="0.3"
android:scaleY="0.3"
android:translateX="89.6"
android:translateY="89.6">
<group android:scaleX="0.24"
android:scaleY="0.24"
android:translateX="97.28"
android:translateY="97.28">
<path
android:fillColor="#ff000000"
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0L228.81,0C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0L14.73,0C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256L137.55,256C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"/>

View File

@@ -3,10 +3,10 @@
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256">
<group android:scaleX="0.3"
android:scaleY="0.3"
android:translateX="89.6"
android:translateY="89.6">
<group android:scaleX="0.24"
android:scaleY="0.24"
android:translateX="97.28"
android:translateY="97.28">
<path
android:fillColor="#ff000000"
android:pathData="M250.09,13.49C251.39,10.51 251.11,7.08 249.33,4.36C247.55,1.64 244.52,0 241.27,0L228.81,0C226.08,0 223.61,1.62 222.51,4.11C211.54,29.1 153.63,160.99 134.29,205.04C133.2,207.54 130.73,209.15 128,209.15C125.27,209.15 122.8,207.54 121.7,205.04C102.36,160.99 44.46,29.1 33.49,4.11C32.39,1.62 29.92,0 27.19,0L14.73,0C11.48,0 8.45,1.64 6.67,4.36C4.89,7.08 4.61,10.51 5.91,13.49C26.64,60.8 95.56,218.1 109.63,250.24C111.17,253.74 114.63,256 118.45,256L137.55,256C141.37,256 144.83,253.74 146.36,250.24C160.44,218.1 229.36,60.8 250.09,13.49Z"/>

View File

@@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="800"
android:viewportHeight="800">
<group android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="200"
android:translateY="200">
<path
android:fillColor="#ff000000"
android:pathData="M400,0c220.77,0 400,179.23 400,400c-0,220.77 -179.23,400 -400,400c-220.77,-0 -400,-179.23 -400,-400c0,-220.77 179.23,-400 400,-400ZM400,36c200.9,-0 364,163.1 364,364c0,200.9 -163.1,364 -364,364c-200.9,0 -364,-163.1 -364,-364c-0,-200.9 163.1,-364 364,-364Z"
android:strokeLineJoin="round"
android:fillType="evenOdd"/>
<path
android:fillColor="#ff000000"
android:pathData="M538.74,269.87c1.48,-3.38 1.16,-7.28 -0.86,-10.37c-2.02,-3.09 -5.46,-4.95 -9.16,-4.95c-5.15,0 -10.44,0 -14.16,0c-3.1,0 -5.91,1.83 -7.15,4.67c-12.47,28.4 -78.27,178.27 -100.25,228.33c-1.25,2.84 -4.05,4.67 -7.15,4.67c-3.1,0 -5.91,-1.83 -7.15,-4.67c-21.98,-50.06 -87.78,-199.93 -100.25,-228.33c-1.25,-2.84 -4.05,-4.67 -7.15,-4.67c-3.73,0 -9.02,0 -14.16,0c-3.69,0 -7.14,1.86 -9.16,4.95c-2.02,3.09 -2.34,6.99 -0.86,10.37c23.56,53.77 101.87,232.52 117.87,269.03c1.74,3.98 5.67,6.55 10.02,6.55c6.29,-0 15.41,-0 21.7,-0c4.34,-0 8.27,-2.57 10.02,-6.55c16,-36.51 94.32,-215.27 117.87,-269.03Z"/>
<path
android:fillColor="#ff000000"
android:pathData="M408.12,395.31c-1.67,2.9 -4.77,4.69 -8.12,4.69c-3.35,-0 -6.44,-1.79 -8.12,-4.69c-17,-29.44 -56.16,-97.26 -73.15,-126.7c-1.67,-2.9 -1.67,-6.47 0,-9.38c1.67,-2.9 4.77,-4.69 8.12,-4.69c33.99,0 112.31,0 146.31,0c3.35,0 6.44,1.79 8.12,4.69c1.67,2.9 1.67,6.47 -0,9.38c-17,29.44 -56.16,97.26 -73.15,126.7Z"/>
</group>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/revanced_adaptive_background_custom" />
<foreground android:drawable="@mipmap/revanced_adaptive_foreground_custom" />
<monochrome android:drawable="@drawable/revanced_adaptive_monochrome_custom" />
</adaptive-icon>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/revanced_adaptive_background_minimal" />
<foreground android:drawable="@drawable/revanced_adaptive_foreground_minimal" />
<monochrome android:drawable="@drawable/revanced_adaptive_monochrome_minimal" />
</adaptive-icon>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/revanced_adaptive_background_rounded" />
<foreground android:drawable="@drawable/revanced_adaptive_foreground_rounded" />
<monochrome android:drawable="@drawable/revanced_adaptive_monochrome_rounded" />
</adaptive-icon>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/revanced_adaptive_background_scaled" />
<foreground android:drawable="@drawable/revanced_adaptive_foreground_scaled" />
<monochrome android:drawable="@drawable/revanced_adaptive_monochrome_scaled" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB