feat(YouTube): Add Change form factor patch (#4217)

This commit is contained in:
LisoUseInAIKyrios 2024-12-27 10:48:14 +04:00 committed by GitHub
parent da4aeac42b
commit 644ac5baa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 365 additions and 156 deletions

View File

@ -153,7 +153,6 @@ public abstract class Setting<T> {
/** /**
* Confirmation message to display, if the user tries to change the setting from the default value. * Confirmation message to display, if the user tries to change the setting from the default value.
* Currently this works only for Boolean setting types.
*/ */
@Nullable @Nullable
public final StringRef userDialogMessage; public final StringRef userDialogMessage;
@ -244,6 +243,7 @@ public abstract class Setting<T> {
* *
* This method will be deleted in the future. * This method will be deleted in the future.
*/ */
@SuppressWarnings("rawtypes")
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) { if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do. return; // Nothing to do.
@ -419,6 +419,7 @@ public abstract class Setting<T> {
boolean rebootSettingChanged = false; boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0; int numberOfSettingsImported = 0;
//noinspection rawtypes
for (Setting setting : SETTINGS) { for (Setting setting : SETTINGS) {
String key = setting.getImportExportKey(); String key = setting.getImportExportKey();
if (json.has(key)) { if (json.has(key)) {

View File

@ -42,7 +42,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { try {
Setting<?> setting = Setting.getSettingFromPath(str); Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
if (setting == null) { if (setting == null) {
return; return;
} }
@ -52,23 +52,21 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
} }
Logger.printDebug(() -> "Preference changed: " + setting.key); Logger.printDebug(() -> "Preference changed: " + setting.key);
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. if (!settingImportInProgress && !showingUserDialogMessage) {
updatePreference(pref, setting, true, settingImportInProgress); if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
// Update any other preference availability that may now be different. // Do not change the setting yet, to allow preserving whatever
updateUIAvailability(); // list/text value was previously set if it needs to be reverted.
showSettingUserDialogConfirmation(pref, setting);
if (settingImportInProgress) { return;
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
} else if (setting.rebootApp) { } else if (setting.rebootApp) {
showRestartDialog(getContext()); showRestartDialog(getContext());
} }
} }
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
} }
@ -92,7 +90,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
} }
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
final var context = getContext(); final var context = getContext();
@ -104,12 +102,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
.setTitle(confirmDialogTitle) .setTitle(confirmDialogTitle)
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString()) .setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> { .setPositiveButton(android.R.string.ok, (dialog, id) -> {
// User confirmed, save to the Setting.
updatePreference(pref, setting, true, false);
// Update availability of other preferences that may be changed.
updateUIAvailability();
if (setting.rebootApp) { if (setting.rebootApp) {
showRestartDialog(context); showRestartDialog(context);
} }
}) })
.setNegativeButton(android.R.string.cancel, (dialog, id) -> { .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. // Restore whatever the setting was before the change.
updatePreference(pref, setting, true, true);
}) })
.setOnDismissListener(dialog -> { .setOnDismissListener(dialog -> {
showingUserDialogMessage = false; showingUserDialogMessage = false;
@ -132,6 +137,24 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
updatePreferenceScreen(getPreferenceScreen(), false, false); updatePreferenceScreen(getPreferenceScreen(), false, false);
} }
/**
* @return If the preference is currently set to the default value of the Setting.
*/
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
if (pref instanceof SwitchPreference switchPref) {
return switchPref.isChecked() == (Boolean) setting.defaultValue;
}
if (pref instanceof EditTextPreference editPreference) {
return editPreference.getText().equals(setting.defaultValue.toString());
}
if (pref instanceof ListPreference listPref) {
return listPref.getValue().equals(setting.defaultValue.toString());
}
throw new IllegalStateException("Must override method to handle "
+ "preference type: " + pref.getClass());
}
/** /**
* Syncs all UI Preferences to any {@link Setting} they represent. * Syncs all UI Preferences to any {@link Setting} they represent.
*/ */
@ -170,23 +193,20 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
protected void syncSettingWithPreference(@NonNull Preference pref, protected void syncSettingWithPreference(@NonNull Preference pref,
@NonNull Setting<?> setting, @NonNull Setting<?> setting,
boolean applySettingToPreference) { boolean applySettingToPreference) {
if (pref instanceof SwitchPreference) { if (pref instanceof SwitchPreference switchPref) {
SwitchPreference switchPref = (SwitchPreference) pref;
BooleanSetting boolSetting = (BooleanSetting) setting; BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) { if (applySettingToPreference) {
switchPref.setChecked(boolSetting.get()); switchPref.setChecked(boolSetting.get());
} else { } else {
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
} }
} else if (pref instanceof EditTextPreference) { } else if (pref instanceof EditTextPreference editPreference) {
EditTextPreference editPreference = (EditTextPreference) pref;
if (applySettingToPreference) { if (applySettingToPreference) {
editPreference.setText(setting.get().toString()); editPreference.setText(setting.get().toString());
} else { } else {
Setting.privateSetValueFromString(setting, editPreference.getText()); Setting.privateSetValueFromString(setting, editPreference.getText());
} }
} else if (pref instanceof ListPreference) { } else if (pref instanceof ListPreference listPref) {
ListPreference listPref = (ListPreference) pref;
if (applySettingToPreference) { if (applySettingToPreference) {
listPref.setValue(setting.get().toString()); listPref.setValue(setting.get().toString());
} else { } else {

View File

@ -176,14 +176,13 @@ public final class AlternativeThumbnailsPatch {
// Unknown tab, treat as the home tab; // Unknown tab, treat as the home tab;
return homeOption; return homeOption;
} }
if (selectedNavButton == NavigationButton.HOME) {
return homeOption; return switch (selectedNavButton) {
} case SUBSCRIPTIONS, NOTIFICATIONS -> subscriptionsOption;
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { case LIBRARY -> libraryOption;
return subscriptionsOption; // Home or explore tab.
} default -> homeOption;
// A library tab variant is active. };
return libraryOption;
} }
/** /**

View File

@ -0,0 +1,54 @@
package app.revanced.extension.youtube.patches;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ChangeFormFactorPatch {
public enum FormFactor {
/**
* Unmodified, and same as un-patched.
*/
DEFAULT(null),
/**
* <pre>
* Some changes include:
* - Explore tab is present.
* - watch history is missing.
* - feed thumbnails fade in.
*/
UNKNOWN(0),
SMALL(1),
LARGE(2),
/**
* Cars with 'Google built-in'.
* Layout seems identical to {@link #UNKNOWN}
* even when using an Android Automotive device.
*/
AUTOMOTIVE(3),
WEARABLE(4);
@Nullable
final Integer formFactorType;
FormFactor(@Nullable Integer formFactorType) {
this.formFactorType = formFactorType;
}
}
@Nullable
private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
/**
* Injection point.
*/
public static int getFormFactor(int original) {
return FORM_FACTOR_TYPE == null
? original
: FORM_FACTOR_TYPE;
}
}

View File

@ -1,16 +0,0 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class TabletLayoutPatch {
private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get();
/**
* Injection point.
*/
public static boolean getTabletLayoutEnabled() {
return TABLET_LAYOUT_ENABLED;
}
}

View File

@ -528,14 +528,13 @@ final class KeywordContentFilter extends Filter {
if (selectedNavButton == null) { if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home. return hideHome; // Unknown tab, treat the same as home.
} }
if (selectedNavButton == NavigationButton.HOME) {
return hideHome; return switch (selectedNavButton) {
} case HOME, EXPLORE -> hideHome;
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { case SUBSCRIPTIONS -> hideSubscriptions;
return hideSubscriptions; // User is in the Library or notifications.
} default -> false;
// User is in the Library or Notifications tab. };
return false;
} }
private void updateStats(boolean videoWasHidden, @Nullable String keyword) { private void updateStats(boolean videoWasHidden, @Nullable String keyword) {

View File

@ -366,7 +366,7 @@ public final class ShortsFilter extends Filter {
} }
return switch (selectedNavButton) { return switch (selectedNavButton) {
case HOME -> hideHome; case HOME, EXPLORE -> hideHome;
case SUBSCRIPTIONS -> hideSubscriptions; case SUBSCRIPTIONS -> hideSubscriptions;
case LIBRARY -> hideHistory; case LIBRARY -> hideHistory;
default -> false; default -> false;

View File

@ -7,6 +7,7 @@ import static app.revanced.extension.shared.settings.Setting.migrateFromOldPrefe
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew; import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
import static app.revanced.extension.shared.settings.Setting.parent; import static app.revanced.extension.shared.settings.Setting.parent;
import static app.revanced.extension.shared.settings.Setting.parentsAny; import static app.revanced.extension.shared.settings.Setting.parentsAny;
import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage; import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode; import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
import static app.revanced.extension.youtube.patches.ForceOriginalAudioPatch.ForceOriginalAudioAvailability; import static app.revanced.extension.youtube.patches.ForceOriginalAudioPatch.ForceOriginalAudioAvailability;
@ -202,12 +203,12 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE); public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE);
// General layout // General layout
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE, public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
"revanced_remove_viewer_discretion_dialog_user_dialog_message"); "revanced_remove_viewer_discretion_dialog_user_dialog_message");
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message");
public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true); public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true);
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true); public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true);
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", IS_19_17_OR_GREATER ? "19.26.42" : "17.33.42", true, parent(SPOOF_APP_VERSION)); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", IS_19_17_OR_GREATER ? "19.26.42" : "17.33.42", true, parent(SPOOF_APP_VERSION));

View File

@ -3,12 +3,15 @@ package app.revanced.extension.youtube.shared;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE;
import android.app.Activity; import android.app.Activity;
import android.os.Build;
import android.view.View; import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@ -242,6 +245,30 @@ public final class NavigationBar {
// Code is added during patching. // Code is added during patching.
} }
/**
* Use the bundled non cairo filled icon instead of a custom icon.
* Use the old non cairo filled icon, which is almost identical to
* the what would be the filled cairo icon.
*/
private static final int fillBellCairoBlack = Utils.getResourceIdentifier(
"yt_fill_bell_black_24", "drawable");
/**
* Injection point.
* Fixes missing drawable.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings({"unchecked", "rawtypes"})
public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) {
if (fillBellCairoBlack != 0) {
// Show a popup informing this fix is no longer needed to those who might care.
if (BaseSettings.DEBUG.get() && enumMap.containsKey(tabActivityCairo)) {
Logger.printException(() -> "YouTube fixed the cairo notification icons");
}
enumMap.putIfAbsent(tabActivityCairo, fillBellCairoBlack);
}
}
public enum NavigationButton { public enum NavigationButton {
HOME("PIVOT_HOME", "TAB_HOME_CAIRO"), HOME("PIVOT_HOME", "TAB_HOME_CAIRO"),
SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"), SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"),
@ -250,6 +277,10 @@ public final class NavigationBar {
* This tab will never be in a selected state, even if the create video UI is on screen. * This tab will never be in a selected state, even if the create video UI is on screen.
*/ */
CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
/**
* Only shown to automotive layout.
*/
EXPLORE("TAB_EXPLORE"),
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"), SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
/** /**
* Notifications tab. Only present when * Notifications tab. Only present when

View File

@ -1101,6 +1101,10 @@ public final class app/revanced/patches/youtube/layout/buttons/overlay/HidePlaye
public static final fun getHidePlayerOverlayButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getHidePlayerOverlayButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
public final class app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatchKt {
public static final fun getChangeFormFactorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatchKt { public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatchKt {
public static final fun getHideEndscreenCardsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getHideEndscreenCardsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
@ -1230,7 +1234,6 @@ public final class app/revanced/patches/youtube/layout/startupshortsreset/Disabl
} }
public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatchKt { public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatchKt {
public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
public static final fun getEnableTabletLayoutPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getEnableTabletLayoutPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }

View File

@ -0,0 +1,75 @@
package app.revanced.patches.youtube.layout.formfactor
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeFormFactorPatch;"
@Suppress("unused")
val changeFormFactorPatch = bytecodePatch(
name = "Change form factor",
description = "Adds an option to change the UI appearance to a phone, tablet, or automotive device.",
) {
dependsOn(
sharedExtensionPatch,
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",
"19.47.53",
),
)
execute {
addResources("youtube", "layout.formfactor.changeFormFactorPatch")
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
ListPreference(
"revanced_change_form_factor",
summaryKey = null,
)
)
createPlayerRequestBodyWithModelFingerprint.method.apply {
val formFactorEnumClass = formFactorEnumConstructorFingerprint.originalClassDef.type
val index = indexOfFirstInstructionOrThrow {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET &&
reference?.definingClass == formFactorEnumClass &&
reference.type == "I"
}
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstructions(
index + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I
move-result v$register
"""
)
}
}
}

View File

@ -0,0 +1,49 @@
package app.revanced.patches.youtube.layout.formfactor
import app.revanced.patcher.fingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
internal val formFactorEnumConstructorFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
strings(
"UNKNOWN_FORM_FACTOR",
"SMALL_FORM_FACTOR",
"LARGE_FORM_FACTOR",
"AUTOMOTIVE_FORM_FACTOR"
)
}
internal val createPlayerRequestBodyWithModelFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("L")
parameters()
opcodes(Opcode.OR_INT_LIT16)
custom { method, _ ->
method.indexOfModelInstruction() >= 0 &&
method.indexOfReleaseInstruction() >= 0
}
}
private fun Method.indexOfModelInstruction() =
indexOfFirstInstruction {
val reference = getReference<FieldReference>()
reference?.definingClass == "Landroid/os/Build;" &&
reference.name == "MODEL" &&
reference.type == "Ljava/lang/String;"
}
internal fun Method.indexOfReleaseInstruction(): Int =
indexOfFirstInstruction {
val reference = getReference<FieldReference>()
reference?.definingClass == "Landroid/os/Build${'$'}VERSION;" &&
reference.name == "RELEASE" &&
reference.type == "Ljava/lang/String;"
}

View File

@ -1,66 +1,9 @@
package app.revanced.patches.youtube.layout.tablet package app.revanced.patches.youtube.layout.tablet
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel import app.revanced.patches.youtube.layout.formfactor.changeFormFactorPatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/TabletLayoutPatch;" @Deprecated("Use 'Change form factor' instead.")
val enableTabletLayoutPatch = bytecodePatch {
val enableTabletLayoutPatch = bytecodePatch( dependsOn(changeFormFactorPatch)
name = "Enable tablet layout",
description = "Adds an option to enable tablet layout.",
) {
dependsOn(
sharedExtensionPatch,
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",
"19.47.53",
),
)
execute {
addResources("youtube", "layout.tablet.enableTabletLayoutPatch")
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
SwitchPreference("revanced_tablet_layout"),
)
getFormFactorFingerprint.method.apply {
val returnIsLargeFormFactorIndex = instructions.lastIndex - 4
val returnIsLargeFormFactorLabel = getInstruction(returnIsLargeFormFactorIndex)
addInstructionsWithLabels(
0,
"""
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getTabletLayoutEnabled()Z
move-result v0
if-nez v0, :is_large_form_factor
""",
ExternalLabel(
"is_large_form_factor",
returnIsLargeFormFactorLabel,
),
)
}
}
} }

View File

@ -1,25 +0,0 @@
package app.revanced.patches.youtube.layout.tablet
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.AccessFlags
import app.revanced.patcher.fingerprint
internal val getFormFactorFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("L")
parameters("Landroid/content/Context;", "Ljava/util/List;")
opcodes(
Opcode.SGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT,
Opcode.IF_EQZ,
Opcode.SGET_OBJECT,
Opcode.RETURN_OBJECT,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT_OBJECT,
Opcode.RETURN_OBJECT,
)
strings("")
}

View File

@ -105,3 +105,15 @@ internal val pivotBarConstructorFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
strings("com.google.android.apps.youtube.app.endpoint.flags") strings("com.google.android.apps.youtube.app.endpoint.flags")
} }
internal val imageEnumConstructorFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
strings("TAB_ACTIVITY_CAIRO")
}
internal val setEnumMapFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
literal {
ytFillBellId
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.patches.youtube.misc.navigation package app.revanced.patches.youtube.misc.navigation
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.PatchException
@ -12,13 +13,16 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_35_or_greater
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
@ -26,6 +30,8 @@ internal var imageOnlyTabResourceId = -1L
private set private set
internal var actionBarSearchResultsViewMicId = -1L internal var actionBarSearchResultsViewMicId = -1L
private set private set
internal var ytFillBellId = -1L
private set
private val navigationBarHookResourcePatch = resourcePatch { private val navigationBarHookResourcePatch = resourcePatch {
dependsOn(resourceMappingPatch) dependsOn(resourceMappingPatch)
@ -33,6 +39,7 @@ private val navigationBarHookResourcePatch = resourcePatch {
execute { execute {
imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"] imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"]
actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"] actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"]
ytFillBellId = resourceMappings["drawable", "yt_fill_bell_black_24"]
} }
} }
@ -144,6 +151,36 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig
"(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V",
) )
} }
// Fix YT bug of notification tab missing the filled icon.
if (is_19_35_or_greater) {
val cairoNotificationEnumReference = with(imageEnumConstructorFingerprint) {
val stringIndex = stringMatches!!.first().index
val cairoNotificationEnumIndex = method.indexOfFirstInstructionOrThrow(stringIndex) {
opcode == Opcode.SPUT_OBJECT
}
method.getInstruction<ReferenceInstruction>(cairoNotificationEnumIndex).reference
}
setEnumMapFingerprint.method.apply {
val enumMapIndex = indexOfFirstInstructionReversedOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.definingClass == "Ljava/util/EnumMap;" &&
reference.name == "put" &&
reference.parameterTypes.firstOrNull() == "Ljava/lang/Enum;"
}
val instruction = getInstruction<FiveRegisterInstruction>(enumMapIndex)
addInstructions(
enumMapIndex + 1,
"""
sget-object v${instruction.registerD}, $cairoNotificationEnumReference
invoke-static { v${instruction.registerC}, v${instruction.registerD} }, $EXTENSION_CLASS_DESCRIPTOR->setCairoNotificationFilledIcon(Ljava/util/EnumMap;Ljava/lang/Enum;)V
"""
)
}
}
} }
} }

View File

@ -8,7 +8,7 @@
<item>iOS TV</item> <item>iOS TV</item>
</string-array> </string-array>
<string-array name="revanced_spoof_video_streams_client_type_entry_values"> <string-array name="revanced_spoof_video_streams_client_type_entry_values">
<!-- Enum names from extension --> <!-- Extension enum names. -->
<item>ANDROID_VR</item> <item>ANDROID_VR</item>
<item>ANDROID_VR_NO_AUTH</item> <item>ANDROID_VR_NO_AUTH</item>
<item>ANDROID_UNPLUGGED</item> <item>ANDROID_UNPLUGGED</item>
@ -148,6 +148,21 @@
<item>17.33.42</item> <item>17.33.42</item>
</string-array> </string-array>
</patch> </patch>
<patch id="layout.formfactor.changeFormFactorPatch">
<string-array name="revanced_change_form_factor_entries">
<item>@string/revanced_change_form_factor_entry_1</item>
<item>@string/revanced_change_form_factor_entry_2</item>
<item>@string/revanced_change_form_factor_entry_3</item>
<item>@string/revanced_change_form_factor_entry_4</item>
</string-array>
<string-array name="revanced_change_form_factor_entry_values">
<!-- Extension enum names. -->
<item>DEFAULT</item>
<item>SMALL</item>
<item>LARGE</item>
<item>AUTOMOTIVE</item>
</string-array>
</patch>
<patch id="layout.player.fullscreen.exitFullscreenPatch"> <patch id="layout.player.fullscreen.exitFullscreenPatch">
<string-array name="revanced_exit_fullscreen_entries"> <string-array name="revanced_exit_fullscreen_entries">
<item>@string/revanced_exit_fullscreen_entry_1</item> <item>@string/revanced_exit_fullscreen_entry_1</item>
@ -174,7 +189,7 @@
<item>@string/revanced_miniplayer_type_entry_6</item> <item>@string/revanced_miniplayer_type_entry_6</item>
</string-array> </string-array>
<string-array name="revanced_miniplayer_type_entry_values"> <string-array name="revanced_miniplayer_type_entry_values">
<!-- Enum names from the extension. --> <!-- Extension enum names. -->
<item>DISABLED</item> <item>DISABLED</item>
<item>DEFAULT</item> <item>DEFAULT</item>
<item>MINIMAL</item> <item>MINIMAL</item>
@ -192,7 +207,7 @@
<item>@string/revanced_miniplayer_type_entry_6</item> <item>@string/revanced_miniplayer_type_entry_6</item>
</string-array> </string-array>
<string-array name="revanced_miniplayer_type_legacy_entry_values"> <string-array name="revanced_miniplayer_type_legacy_entry_values">
<!-- Enum names from the extension. --> <!-- Extension enum names. -->
<item>DEFAULT</item> <item>DEFAULT</item>
<item>MINIMAL</item> <item>MINIMAL</item>
<item>TABLET</item> <item>TABLET</item>
@ -221,7 +236,7 @@
<item>@string/revanced_change_start_page_entry_browse</item> <item>@string/revanced_change_start_page_entry_browse</item>
</string-array> </string-array>
<string-array name="revanced_change_start_page_entry_values"> <string-array name="revanced_change_start_page_entry_values">
<!-- Enum names from extension --> <!-- Extension enum names. -->
<item>DEFAULT</item> <item>DEFAULT</item>
<!-- Intent Action --> <!-- Intent Action -->
<item>SEARCH</item> <item>SEARCH</item>
@ -248,7 +263,7 @@
<item>@string/revanced_shorts_player_type_regular_player</item> <item>@string/revanced_shorts_player_type_regular_player</item>
</string-array> </string-array>
<string-array name="revanced_shorts_player_type_legacy_entry_values"> <string-array name="revanced_shorts_player_type_legacy_entry_values">
<!-- Enum names from extension --> <!-- Extension enum names. -->
<item>SHORTS_PLAYER</item> <item>SHORTS_PLAYER</item>
<item>REGULAR_PLAYER</item> <item>REGULAR_PLAYER</item>
</string-array> </string-array>
@ -272,7 +287,7 @@
<item>@string/revanced_alt_thumbnail_options_entry_4</item> <item>@string/revanced_alt_thumbnail_options_entry_4</item>
</string-array> </string-array>
<string-array name="revanced_alt_thumbnail_options_entry_values"> <string-array name="revanced_alt_thumbnail_options_entry_values">
<!-- Enum names from the extension --> <!-- Extension enum names. -->
<item>ORIGINAL</item> <item>ORIGINAL</item>
<item>DEARROW</item> <item>DEARROW</item>
<item>DEARROW_STILL_IMAGES</item> <item>DEARROW_STILL_IMAGES</item>

View File

@ -1029,6 +1029,23 @@ Ready to submit?"</string>
<string name="revanced_sb_about_api">sponsor.ajay.app</string> <string name="revanced_sb_about_api">sponsor.ajay.app</string>
<string name="revanced_sb_about_api_sum">Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms</string> <string name="revanced_sb_about_api_sum">Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms</string>
</patch> </patch>
<patch id="layout.formfactor.changeFormFactorPatch">
<string name="revanced_change_form_factor_title">Layout form factor</string>
<string name="revanced_change_form_factor_entry_1">Default</string>
<string name="revanced_change_form_factor_entry_2">Phone</string>
<string name="revanced_change_form_factor_entry_3">Tablet</string>
<string name="revanced_change_form_factor_entry_4">Automotive</string>
<string name="revanced_change_form_factor_user_dialog_message">"Changes include:
Tablet layout
• Community posts are hidden
Automotive layout
• Watch history menu is hidden
• Explore tab is restored
• Shorts open in the regular player
• Feed is organized by topics and channel"</string>
</patch>
<patch id="layout.spoofappversion.spoofAppVersionPatch"> <patch id="layout.spoofappversion.spoofAppVersionPatch">
<string name="revanced_spoof_app_version_title">Spoof app version</string> <string name="revanced_spoof_app_version_title">Spoof app version</string>
<string name="revanced_spoof_app_version_summary_on">Version spoofed</string> <string name="revanced_spoof_app_version_summary_on">Version spoofed</string>
@ -1086,12 +1103,6 @@ If later turned off, it is recommended to clear the app data to prevent UI bugs.
<string name="revanced_shorts_autoplay_background_summary_on">Shorts background play will autoplay</string> <string name="revanced_shorts_autoplay_background_summary_on">Shorts background play will autoplay</string>
<string name="revanced_shorts_autoplay_background_summary_off">Shorts background play will repeat</string> <string name="revanced_shorts_autoplay_background_summary_off">Shorts background play will repeat</string>
</patch> </patch>
<patch id="layout.tablet.enableTabletLayoutPatch">
<string name="revanced_tablet_layout_title">Enable tablet layout</string>
<string name="revanced_tablet_layout_summary_on">Tablet layout is enabled</string>
<string name="revanced_tablet_layout_summary_off">Tablet layout is disabled</string>
<string name="revanced_tablet_layout_user_dialog_message">Community posts do not show up on tablet layouts</string>
</patch>x
<patch id="layout.miniplayer.miniplayerPatch"> <patch id="layout.miniplayer.miniplayerPatch">
<string name="revanced_miniplayer_screen_title">Miniplayer</string> <string name="revanced_miniplayer_screen_title">Miniplayer</string>
<string name="revanced_miniplayer_screen_summary">Change the style of the in app minimized player</string> <string name="revanced_miniplayer_screen_summary">Change the style of the in app minimized player</string>