Skip to content

Commit a50f3b5

Browse files
feat(Custom branding): Add in-app settings to change icon and name (#6059)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
1 parent 64d22a9 commit a50f3b5

File tree

89 files changed

+923
-215
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+923
-215
lines changed

extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@
3131

3232
@SuppressWarnings("unused")
3333
public class GmsCoreSupport {
34-
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
35-
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
36-
3734
private static final String GMS_CORE_PACKAGE_NAME
3835
= getGmsCoreVendorGroupId() + ".android.gms";
3936
private static final Uri GMS_CORE_PROVIDER
@@ -53,6 +50,20 @@ public class GmsCoreSupport {
5350
@Nullable
5451
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
5552

53+
private static String getOriginalPackageName() {
54+
return null; // Modified during patching.
55+
}
56+
57+
/**
58+
* @return If the current package name is the same as the original unpatched app.
59+
* If `GmsCore support` was not included during patching, this returns true;
60+
*/
61+
public static boolean isPackageNameOriginal() {
62+
String originalPackageName = getOriginalPackageName();
63+
return originalPackageName == null
64+
|| originalPackageName.equals(Utils.getContext().getPackageName());
65+
}
66+
5667
private static void open(String queryOrLink) {
5768
Logger.printInfo(() -> "Opening link: " + queryOrLink);
5869

@@ -113,11 +124,10 @@ public static void checkGmsCore(Activity context) {
113124
// Verify the user has not included GmsCore for a root installation.
114125
// GmsCore Support changes the package name, but with a mounted installation
115126
// all manifest changes are ignored and the original package name is used.
116-
String packageName = context.getPackageName();
117-
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
127+
if (isPackageNameOriginal()) {
118128
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
119-
// Cannot use localize text here, since the app will load
120-
// resources from the unpatched app and all patch strings are missing.
129+
// Cannot use localize text here, since the app will load resources
130+
// from the unpatched app and all patch strings are missing.
121131
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
122132

123133
// Do not exit. If the app exits before launch completes (and without
@@ -250,8 +260,8 @@ private static String getGmsCoreDownload() {
250260
};
251261
}
252262

253-
// Modified by a patch. Do not touch.
254263
private static String getGmsCoreVendorGroupId() {
255-
return "app.revanced";
264+
// Modified during patching.
265+
throw new IllegalStateException();
256266
}
257267
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package app.revanced.extension.shared.patches;
2+
3+
import android.content.ComponentName;
4+
import android.content.Context;
5+
import android.content.pm.PackageManager;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
import app.revanced.extension.shared.GmsCoreSupport;
11+
import app.revanced.extension.shared.Logger;
12+
import app.revanced.extension.shared.Utils;
13+
import app.revanced.extension.shared.settings.BaseSettings;
14+
15+
/**
16+
* Patch shared by YouTube and YT Music.
17+
*/
18+
@SuppressWarnings("unused")
19+
public class CustomBrandingPatch {
20+
21+
// Important: In the future, additional branding themes can be added but all existing and prior
22+
// themes cannot be removed or renamed.
23+
//
24+
// This is because if a user has a branding theme selected, then only that launch alias is enabled.
25+
// If a future update removes or renames that alias, then after updating the app is effectively
26+
// broken and it cannot be opened and not even clearing the app data will fix it.
27+
// In that situation the only fix is to completely uninstall and reinstall again.
28+
//
29+
// The most that can be done is to hide a theme from the UI and keep the alias with dummy data.
30+
public enum BrandingTheme {
31+
/**
32+
* Original unpatched icon. Must be first enum.
33+
*/
34+
ORIGINAL("revanced_original"),
35+
ROUNDED("revanced_rounded"),
36+
MINIMAL("revanced_minimal"),
37+
SCALED("revanced_scaled"),
38+
/**
39+
* User provided custom icon. Must be the last enum.
40+
*/
41+
CUSTOM("revanced_custom");
42+
43+
public final String themeAlias;
44+
45+
BrandingTheme(String themeAlias) {
46+
this.themeAlias = themeAlias;
47+
}
48+
49+
private String packageAndNameIndexToClassAlias(String packageName, int appIndex) {
50+
if (appIndex <= 0) {
51+
throw new IllegalArgumentException("App index starts at index 1");
52+
}
53+
return packageName + '.' + themeAlias + '_' + appIndex;
54+
}
55+
}
56+
57+
/**
58+
* Injection point.
59+
*
60+
* The total number of app name aliases, including dummy aliases.
61+
*/
62+
private static int numberOfPresetAppNames() {
63+
// Modified during patching.
64+
throw new IllegalStateException();
65+
}
66+
67+
/**
68+
* Injection point.
69+
*/
70+
@SuppressWarnings("ConstantConditions")
71+
public static void setBranding() {
72+
try {
73+
if (GmsCoreSupport.isPackageNameOriginal()) {
74+
Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon");
75+
return;
76+
}
77+
78+
Context context = Utils.getContext();
79+
PackageManager pm = context.getPackageManager();
80+
String packageName = context.getPackageName();
81+
82+
BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get();
83+
final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get();
84+
ComponentName componentToEnable = null;
85+
ComponentName defaultComponent = null;
86+
List<ComponentName> componentsToDisable = new ArrayList<>();
87+
88+
for (BrandingTheme theme : BrandingTheme.values()) {
89+
// Must always update all aliases including custom alias (last index).
90+
final int numberOfPresetAppNames = numberOfPresetAppNames();
91+
92+
// App name indices starts at 1.
93+
for (int index = 1; index <= numberOfPresetAppNames; index++) {
94+
String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index);
95+
ComponentName component = new ComponentName(packageName, aliasClass);
96+
if (defaultComponent == null) {
97+
// Default is always the first alias.
98+
defaultComponent = component;
99+
}
100+
101+
if (index == selectedNameIndex && theme == selectedBranding) {
102+
componentToEnable = component;
103+
} else {
104+
componentsToDisable.add(component);
105+
}
106+
}
107+
}
108+
109+
if (componentToEnable == null) {
110+
// User imported a bad app name index value. Either the imported data
111+
// was corrupted, or they previously had custom name enabled and the app
112+
// no longer has a custom name specified.
113+
Utils.showToastLong("Custom branding reset");
114+
BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault();
115+
BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault();
116+
117+
componentToEnable = defaultComponent;
118+
componentsToDisable.remove(defaultComponent);
119+
}
120+
121+
for (ComponentName disable : componentsToDisable) {
122+
// Use info logging because if the alias status become corrupt the app cannot launch.
123+
Logger.printInfo(() -> "Disabling: " + disable.getClassName());
124+
pm.setComponentEnabledSetting(disable,
125+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
126+
}
127+
128+
ComponentName componentToEnableFinal = componentToEnable;
129+
Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName());
130+
pm.setComponentEnabledSetting(componentToEnable,
131+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
132+
} catch (Exception ex) {
133+
Logger.printException(() -> "setBranding failure", ex);
134+
}
135+
}
136+
}

extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.lang.Boolean.FALSE;
44
import static java.lang.Boolean.TRUE;
5+
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
56
import static app.revanced.extension.shared.settings.Setting.parent;
67
import static app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.AudioStreamLanguageOverrideAvailability;
78

@@ -40,4 +41,7 @@ public class BaseSettings {
4041
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
4142

4243
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
44+
45+
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
46+
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
4347
}

extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static app.revanced.extension.shared.requests.Route.Method.GET;
66

77
import android.annotation.SuppressLint;
8+
import android.app.Activity;
89
import android.app.Dialog;
910
import android.app.ProgressDialog;
1011
import android.content.Context;
@@ -125,6 +126,8 @@ private String createDialogHtml(WebLink[] aboutLinks) {
125126

126127
{
127128
setOnPreferenceClickListener(pref -> {
129+
Context context = pref.getContext();
130+
128131
// Show a progress spinner if the social links are not fetched yet.
129132
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
130133
// Show a progress spinner, but only if the api fetch takes more than a half a second.
@@ -137,17 +140,18 @@ private String createDialogHtml(WebLink[] aboutLinks) {
137140
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
138141

139142
Utils.runOnBackgroundThread(() ->
140-
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
143+
fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress));
141144
} else {
142145
// No network call required and can run now.
143-
fetchLinksAndShowDialog(null, null, null);
146+
fetchLinksAndShowDialog(context, null, null, null);
144147
}
145148

146149
return false;
147150
});
148151
}
149152

150-
private void fetchLinksAndShowDialog(@Nullable Handler handler,
153+
private void fetchLinksAndShowDialog(Context context,
154+
@Nullable Handler handler,
151155
Runnable showDialogRunnable,
152156
@Nullable ProgressDialog progress) {
153157
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
@@ -164,7 +168,17 @@ private void fetchLinksAndShowDialog(@Nullable Handler handler,
164168
if (handler != null) {
165169
handler.removeCallbacks(showDialogRunnable);
166170
}
167-
if (progress != null) {
171+
172+
// Don't continue if the activity is done. To test this tap the
173+
// about dialog and immediately press back before the dialog can show.
174+
if (context instanceof Activity activity) {
175+
if (activity.isFinishing() || activity.isDestroyed()) {
176+
Logger.printDebug(() -> "Not showing about dialog, activity is closed");
177+
return;
178+
}
179+
}
180+
181+
if (progress != null && progress.isShowing()) {
168182
progress.dismiss();
169183
}
170184
new WebViewDialog(getContext(), htmlDialog).show();

patches/api/patches.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,7 @@ public final class app/revanced/util/ResourceUtilsKt {
19191919
public static final fun forEachChildElement (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V
19201920
public static final fun insertFirst (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V
19211921
public static final fun iterateXmlNodeChildren (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
1922+
public static final fun removeFromParent (Lorg/w3c/dom/Node;)Lorg/w3c/dom/Node;
19221923
}
19231924

19241925
public final class app/revanced/util/resource/ArrayResource : app/revanced/util/resource/BaseResource {

patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
44
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
55
import app.revanced.patcher.patch.bytecodePatch
66
import app.revanced.patcher.util.smali.ExternalLabel
7+
import app.revanced.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME
8+
import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME
9+
import app.revanced.patches.music.misc.gms.musicActivityOnCreateFingerprint
10+
import app.revanced.patches.music.misc.settings.PreferenceScreen
711
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
812
import app.revanced.patches.shared.misc.mapping.get
913
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
@@ -50,24 +54,18 @@ private val disableSplashAnimationPatch = bytecodePatch {
5054
}
5155
}
5256

53-
private const val APP_NAME = "YT Music ReVanced"
54-
5557
@Suppress("unused")
5658
val customBrandingPatch = baseCustomBrandingPatch(
57-
defaultAppName = APP_NAME,
58-
appNameValues = mapOf(
59-
"YT Music ReVanced" to APP_NAME,
60-
"Music ReVanced" to "Music ReVanced",
61-
"Music" to "Music",
62-
"YT Music" to "YT Music",
63-
),
64-
resourceFolder = "custom-branding/music",
65-
iconResourceFileNames = arrayOf(
66-
"adaptiveproduct_youtube_music_2024_q4_background_color_108",
67-
"adaptiveproduct_youtube_music_2024_q4_foreground_color_108",
68-
"ic_launcher_release",
69-
),
70-
monochromeIconFileNames = arrayOf("ic_app_icons_themed_youtube_music.xml"),
59+
addResourcePatchName = "music",
60+
originalLauncherIconName = "ic_launcher_release",
61+
originalAppName = "@string/app_launcher_name",
62+
originalAppPackageName = MUSIC_PACKAGE_NAME,
63+
copyExistingIntentsToAliases = false,
64+
numberOfPresetAppNames = 5,
65+
mainActivityOnCreateFingerprint = musicActivityOnCreateFingerprint,
66+
mainActivityName = MUSIC_MAIN_ACTIVITY_NAME,
67+
activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME,
68+
preferenceScreen = PreferenceScreen.GENERAL,
7169

7270
block = {
7371
dependsOn(disableSplashAnimationPatch)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package app.revanced.patches.music.misc.gms
22

33
object Constants {
4+
internal const val MUSIC_MAIN_ACTIVITY_NAME = "com.google.android.apps.youtube.music.activities.MusicActivity"
5+
46
internal const val REVANCED_MUSIC_PACKAGE_NAME = "app.revanced.android.apps.youtube.music"
57
internal const val MUSIC_PACKAGE_NAME = "com.google.android.apps.youtube.music"
68
}

0 commit comments

Comments
 (0)