feat(YouTube): Add Change form factor, Remove Change layout patch

This commit is contained in:
inotia00 2025-03-14 18:37:56 +09:00
parent 1b3ebe1a58
commit 86e5c1b45c
22 changed files with 579 additions and 212 deletions

View File

@ -24,9 +24,6 @@ import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef; import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
/**
* @noinspection rawtypes
*/
@SuppressWarnings("unused") @SuppressWarnings("unused")
public abstract class Setting<T> { public abstract class Setting<T> {
@ -128,6 +125,7 @@ public abstract class Setting<T> {
/** /**
* @return All settings that have been created, sorted by keys. * @return All settings that have been created, sorted by keys.
* @noinspection Java8ListSort
*/ */
@NonNull @NonNull
private static List<Setting<?>> allLoadedSettingsSorted() { private static List<Setting<?>> allLoadedSettingsSorted() {
@ -171,7 +169,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;
@ -271,6 +268,7 @@ public abstract class Setting<T> {
* <p> * <p>
* 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.
@ -452,6 +450,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

@ -12,6 +12,7 @@ import android.preference.EditTextPreference;
import android.preference.ListPreference; import android.preference.ListPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
@ -21,6 +22,9 @@ import android.widget.ListView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
@ -48,10 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { try {
if (str == null) { Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
return;
}
Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) { if (setting == null) {
return; return;
} }
@ -59,24 +60,23 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (pref == null) { if (pref == null) {
return; return;
} }
Logger.printDebug(() -> "Preference changed: " + setting.key);
if (!settingImportInProgress && !showingUserDialogMessage) {
if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) {
// Do not change the setting yet, to allow preserving whatever
// list/text value was previously set if it needs to be reverted.
showSettingUserDialogConfirmation(pref, setting);
return;
} else if (setting.rebootApp) {
showRestartDialog(getContext());
}
}
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress); updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different. // Update any other preference availability that may now be different.
updateUIAvailability(); updateUIAvailability();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
} else if (setting.rebootApp) {
showRestartDialog(getActivity());
}
}
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
} }
@ -90,14 +90,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* so all app specific {@link Setting} instances are loaded before this method returns. * so all app specific {@link Setting} instances are loaded before this method returns.
*/ */
protected void initialize() { protected void initialize() {
final int id = getXmlIdentifier("revanced_prefs"); final int identifier = getXmlIdentifier("revanced_prefs");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
if (id == 0) return; PreferenceScreen screen = getPreferenceScreen();
addPreferencesFromResource(id); Utils.sortPreferenceGroups(screen);
Utils.sortPreferenceGroups(getPreferenceScreen()); Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
} }
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { private void showSettingUserDialogConfirmation(Preference pref, Setting<?> setting) {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
final var context = getActivity(); final var context = getActivity();
@ -107,12 +109,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
.setTitle(android.R.string.dialog_alert_title) .setTitle(android.R.string.dialog_alert_title)
.setMessage(setting.userDialogMessage.toString()) .setMessage(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 -> showingUserDialogMessage = false) .setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false) .setCancelable(false)
@ -123,7 +132,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Updates all Preferences values and their availability using the current values in {@link Setting}. * Updates all Preferences values and their availability using the current values in {@link Setting}.
*/ */
protected void updateUIToSettingValues() { protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true, true); updatePreferenceScreen(getPreferenceScreen(), true,true);
} }
/** /**
@ -133,24 +142,48 @@ 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.
*/ */
private void updatePreferenceScreen(@NonNull PreferenceScreen screen, private void updatePreferenceScreen(@NonNull PreferenceGroup group,
boolean syncSettingValue, boolean syncSettingValue,
boolean applySettingToPreference) { boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences, // Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check // but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences. // the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) { for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i); Preference pref = group.getPreference(i);
if (pref instanceof PreferenceScreen preferenceScreen) { if (pref instanceof PreferenceGroup subGroup) {
updatePreferenceScreen(preferenceScreen, syncSettingValue, applySettingToPreference); updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) { } else if (pref.hasKey()) {
String key = pref.getKey(); String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key); Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) { if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference); updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
} else if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && (pref instanceof SwitchPreference
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
// Probably a typo in the patches preference declaration.
Logger.printException(() -> "Preference key has no setting: " + key);
} }
} }
} }
@ -166,26 +199,26 @@ 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 switchPreference) { if (pref instanceof SwitchPreference switchPref) {
BooleanSetting boolSetting = (BooleanSetting) setting; BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) { if (applySettingToPreference) {
switchPreference.setChecked(boolSetting.get()); switchPref.setChecked(boolSetting.get());
} else { } else {
BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked()); BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
} }
} else if (pref instanceof EditTextPreference editTextPreference) { } else if (pref instanceof EditTextPreference editPreference) {
if (applySettingToPreference) { if (applySettingToPreference) {
editTextPreference.setText(setting.get().toString()); editPreference.setText(setting.get().toString());
} else { } else {
Setting.privateSetValueFromString(setting, editTextPreference.getText()); Setting.privateSetValueFromString(setting, editPreference.getText());
} }
} else if (pref instanceof ListPreference listPreference) { } else if (pref instanceof ListPreference listPref) {
if (applySettingToPreference) { if (applySettingToPreference) {
listPreference.setValue(setting.get().toString()); listPref.setValue(setting.get().toString());
} else { } else {
Setting.privateSetValueFromString(setting, listPreference.getValue()); Setting.privateSetValueFromString(setting, listPref.getValue());
} }
updateListPreferenceSummary(listPreference, setting); updateListPreferenceSummary(listPref, setting);
} else { } else {
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref); Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
} }
@ -194,7 +227,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/** /**
* Updates a UI Preference with the {@link Setting} that backs it. * Updates a UI Preference with the {@link Setting} that backs it.
* *
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference * @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference. * @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference. * If false, then apply {@link Setting} <- Preference.
*/ */

View File

@ -3,6 +3,8 @@ package app.revanced.extension.shared.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment; import android.app.Fragment;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
@ -13,6 +15,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.preference.Preference; import android.preference.Preference;
@ -43,11 +46,11 @@ import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue; import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import kotlin.text.Regex;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class Utils { public class Utils {
@ -529,6 +532,81 @@ public class Utils {
} }
} }
/**
* Ignore this class. It must be public to satisfy Android requirements.
*/
public static final class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/** /**
* Safe to call from any thread * Safe to call from any thread
*/ */
@ -737,14 +815,14 @@ public class Utils {
} }
} }
private static final Regex punctuationRegex = new Regex("\\p{P}+"); private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
/** /**
* Strips all punctuation and converts to lower case. A null parameter returns an empty string. * Strips all punctuation and converts to lower case. A null parameter returns an empty string.
*/ */
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
if (original == null) return ""; if (original == null) return "";
return punctuationRegex.replace(original, "").toLowerCase(); return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
} }
/** /**
@ -763,8 +841,8 @@ public class Utils {
Preference preference = group.getPreference(i); Preference preference = group.getPreference(i);
final Sort preferenceSort; final Sort preferenceSort;
if (preference instanceof PreferenceGroup preferenceGroup) { if (preference instanceof PreferenceGroup subGroup) {
sortPreferenceGroups(preferenceGroup); sortPreferenceGroups(subGroup);
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
} else { } else {
// Allow individual preferences to set a key sorting. // Allow individual preferences to set a key sorting.
@ -774,13 +852,16 @@ public class Utils {
final String sortValue; final String sortValue;
switch (preferenceSort) { switch (preferenceSort) {
case BY_TITLE -> case BY_TITLE:
sortValue = removePunctuationConvertToLowercase(preference.getTitle()); sortValue = removePunctuationConvertToLowercase(preference.getTitle());
case BY_KEY -> sortValue = preference.getKey(); break;
case UNSORTED -> { case BY_KEY:
sortValue = preference.getKey();
break;
case UNSORTED:
continue; // Keep original sorting. continue; // Keep original sorting.
} default:
default -> throw new IllegalStateException(); throw new IllegalStateException();
} }
preferences.put(sortValue, preference); preferences.put(sortValue, preference);
@ -790,7 +871,7 @@ public class Utils {
for (Preference pref : preferences.values()) { for (Preference pref : preferences.values()) {
int order = index++; int order = index++;
// If the preference is a PreferenceScreen or is an intent preference, move to the top. // Move any screens, intents, and the one off About preference to the top.
if (pref instanceof PreferenceScreen || pref.getIntent() != null) { if (pref instanceof PreferenceScreen || pref.getIntent() != null) {
// Arbitrary high number. // Arbitrary high number.
order -= 1000; order -= 1000;
@ -799,4 +880,32 @@ public class Utils {
pref.setOrder(order); pref.setOrder(order);
} }
} }
/**
* Set all preferences to multiline titles if the device is not using an English variant.
* The English strings are heavily scrutinized and all titles fit on screen
* except 2 or 3 preference strings and those do not affect readability.
* <p>
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
* and visually it looks better to clip the text and keep all titles 1 line.
*/
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
if (!isSDKAbove(26)) {
return;
}
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
return;
}
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
pref.setSingleLineTitle(false);
if (pref instanceof PreferenceGroup subGroup) {
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
}
}
}
} }

View File

@ -189,14 +189,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

@ -556,14 +556,13 @@ public 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

@ -0,0 +1,151 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.view.View;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView;
@SuppressWarnings("unused")
public class ChangeFormFactorPatch {
public enum FormFactor {
/**
* Unmodified, and same as un-patched.
*/
DEFAULT(null, null, null),
/**
* <pre>
* Some changes include:
* - Explore tab is present.
* - watch history is missing.
* - feed thumbnails fade in.
*/
UNKNOWN(0, null, null),
SMALL(1, null, TRUE),
SMALL_WIDTH_DP(1, 480, TRUE),
LARGE(2, null, FALSE),
LARGE_WIDTH_DP(2, 600, FALSE),
/**
* Cars with 'Google built-in'.
* Layout seems identical to {@link #UNKNOWN}
* even when using an Android Automotive device.
*/
AUTOMOTIVE(3, null, null),
WEARABLE(4, null, null);
@Nullable
final Integer formFactorType;
@Nullable
final Integer widthDp;
@Nullable
final Boolean setMinimumDp;
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
this.formFactorType = formFactorType;
this.widthDp = widthDp;
this.setMinimumDp = setMinimumDp;
}
private boolean setMinimumDp() {
return BooleanUtils.isTrue(setMinimumDp);
}
}
private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
@Nullable
private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType;
private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
/**
* Injection point.
*/
public static int getFormFactor(int original) {
if (FORM_FACTOR_TYPE == null) return original;
if (USING_AUTOMOTIVE_TYPE) {
// Do not change if the player is opening or is opened,
// otherwise the video description cannot be opened.
PlayerType current = PlayerType.getCurrent();
if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
Logger.printDebug(() -> "Using original form factor for player");
return original;
}
if (!RootView.isSearchBarActive()) {
// Automotive type shows error 400 when opening a channel page and using some explore tab.
// This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
// Work around the issue by using the original form factor if not in search and the
// navigation back button is present.
if (RootView.isBackButtonVisible()) {
Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
return original;
}
// Do not change library tab otherwise watch history is hidden.
// Do this check last since the current navigation button is required.
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
return original;
}
}
}
return FORM_FACTOR_TYPE;
}
/**
* Injection point.
*/
public static int getWidthDp(int original) {
if (FORM_FACTOR_TYPE == null) return original;
Integer widthDp = FORM_FACTOR.widthDp;
if (widthDp == null) {
return original;
}
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
if (smallestScreenWidthDp == 0) {
return original;
}
return FORM_FACTOR.setMinimumDp()
? Math.min(smallestScreenWidthDp, widthDp)
: Math.max(smallestScreenWidthDp, widthDp);
}
public static boolean phoneLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 1);
}
public static boolean tabletLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 2);
}
/**
* Injection point.
*/
public static void navigationTabCreated(NavigationButton button, View tabView) {
// On first startup of the app the navigation buttons are fetched and updated.
// If the user immediately opens the 'You' or opens a video, then the call to
// update the navigtation buttons will use the non automotive form factor
// and the explore tab is missing.
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
// For now, always hide the explore tab.
if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
tabView.setVisibility(View.GONE);
}
}
}

View File

@ -1,79 +0,0 @@
package app.revanced.extension.youtube.patches.general;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Objects;
import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class LayoutSwitchPatch {
public enum FormFactor {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL(null, null, null),
SMALL_FORM_FACTOR(1, null, TRUE),
SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
LARGE_FORM_FACTOR(2, null, FALSE),
LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
@Nullable
final Integer formFactorType;
@Nullable
final Integer widthDp;
@Nullable
final Boolean setMinimumDp;
FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
this.formFactorType = formFactorType;
this.widthDp = widthDp;
this.setMinimumDp = setMinimumDp;
}
private boolean setMinimumDp() {
return BooleanUtils.isTrue(setMinimumDp);
}
}
private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
public static int getFormFactor(int original) {
Integer formFactorType = FORM_FACTOR.formFactorType;
return formFactorType == null
? original
: formFactorType;
}
public static int getWidthDp(int original) {
Integer widthDp = FORM_FACTOR.widthDp;
if (widthDp == null) {
return original;
}
final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
if (smallestScreenWidthDp == 0) {
return original;
}
return FORM_FACTOR.setMinimumDp()
? Math.min(smallestScreenWidthDp, widthDp)
: Math.max(smallestScreenWidthDp, widthDp);
}
public static boolean phoneLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 1);
}
public static boolean tabletLayoutEnabled() {
return Objects.equals(FORM_FACTOR.formFactorType, 2);
}
}

View File

@ -32,9 +32,9 @@ import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeT
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability; import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption; import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch; import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage; import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch; import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode; import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode;
import app.revanced.extension.youtube.patches.player.MiniplayerPatch; import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
@ -154,7 +154,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE); public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", 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);
public static final EnumSetting<FormFactor> CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true); 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 CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true); public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
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 StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION)); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));

View File

@ -75,7 +75,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { try {
if (str == null) return; if (str == null) return;
Setting<?> setting = Setting.getSettingFromPath(str); Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
if (setting == null) return; if (setting == null) return;

View File

@ -8,7 +8,7 @@ import android.preference.Preference;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch; import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch; import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
@ -82,7 +82,7 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
*/ */
private static void TabletLayoutLinks() { private static void TabletLayoutLinks() {
final boolean isTablet = ExtendedUtils.isTablet() && final boolean isTablet = ExtendedUtils.isTablet() &&
!LayoutSwitchPatch.phoneLayoutEnabled(); !ChangeFormFactorPatch.phoneLayoutEnabled();
enableDisablePreferences( enableDisablePreferences(
isTablet, isTablet,

View File

@ -223,6 +223,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

@ -2,9 +2,48 @@ package app.revanced.extension.youtube.shared;
import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible; import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible;
import android.graphics.drawable.Drawable;
import android.widget.FrameLayout;
import java.lang.ref.WeakReference;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class RootView { public final class RootView {
/**
* Interface to call obfuscated methods in AppCompat Toolbar class.
*/
public interface AppCompatToolbarPatchInterface {
Drawable patch_getToolbarIcon();
}
private static volatile WeakReference<AppCompatToolbarPatchInterface> toolbarResultsRef
= new WeakReference<>(null);
/**
* Injection point.
*/
public static void setToolbar(FrameLayout layout) {
AppCompatToolbarPatchInterface toolbar = Utils.getChildView(layout, false, (view) ->
view instanceof AppCompatToolbarPatchInterface
);
if (toolbar == null) {
Logger.printException(() -> "Could not find toolbar");
return;
}
toolbarResultsRef = new WeakReference<>(toolbar);
}
public static boolean isBackButtonVisible() {
AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get();
return toolbar != null && toolbar.patch_getToolbarIcon() != null;
}
/** /**
* @return If the search bar is on screen. This includes if the player * @return If the search bar is on screen. This includes if the player
* is on screen and the search results are behind the player (and not visible). * is on screen and the search results are behind the player (and not visible).

View File

@ -1,4 +1,4 @@
package app.revanced.patches.youtube.general.layoutswitch package app.revanced.patches.youtube.general.formfactor
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
@ -6,30 +6,37 @@ import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LAYOUT import app.revanced.patches.youtube.utils.navigation.hookNavigationButtonCreated
import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch
import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_FORM_FACTOR
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.fingerprint.definingClassOrThrow import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
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 com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
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.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
"$GENERAL_PATH/LayoutSwitchPatch;" "$GENERAL_PATH/ChangeFormFactorPatch;"
@Suppress("unused") @Suppress("unused")
val layoutSwitchPatch = bytecodePatch( val changeFormFactorPatch = bytecodePatch(
CHANGE_LAYOUT.title, CHANGE_FORM_FACTOR.title,
CHANGE_LAYOUT.summary, CHANGE_FORM_FACTOR.summary,
) { ) {
compatibleWith(COMPATIBLE_PACKAGE) compatibleWith(COMPATIBLE_PACKAGE)
dependsOn(settingsPatch) dependsOn(
settingsPatch,
playerTypeHookPatch,
navigationBarHookPatch,
)
execute { execute {
@ -53,27 +60,31 @@ val layoutSwitchPatch = bytecodePatch(
) )
} }
layoutSwitchFingerprint.methodOrThrow().apply { widthDpUIFingerprint.matchOrThrow().let {
val index = indexOfFirstInstructionReversedOrThrow(Opcode.IF_NEZ) it.method.apply {
val register = getInstruction<OneRegisterInstruction>(index).registerA val index = it.patternMatch!!.startIndex
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions( addInstructions(
index, """ index, """
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I
move-result v$register move-result v$register
""" """
) )
}
} }
hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
// region add settings // region add settings
addPreference( addPreference(
arrayOf( arrayOf(
"PREFERENCE_SCREEN: GENERAL", "PREFERENCE_SCREEN: GENERAL",
"PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS", "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS",
"SETTINGS: CHANGE_LAYOUT" "SETTINGS: CHANGE_FORM_FACTOR"
), ),
CHANGE_LAYOUT CHANGE_FORM_FACTOR
) )
// endregion // endregion

View File

@ -1,4 +1,4 @@
package app.revanced.patches.youtube.general.layoutswitch package app.revanced.patches.youtube.general.formfactor
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or import app.revanced.util.or
@ -11,20 +11,16 @@ internal val formFactorEnumConstructorFingerprint = legacyFingerprint(
strings = listOf( strings = listOf(
"UNKNOWN_FORM_FACTOR", "UNKNOWN_FORM_FACTOR",
"SMALL_FORM_FACTOR", "SMALL_FORM_FACTOR",
"LARGE_FORM_FACTOR" "LARGE_FORM_FACTOR",
"AUTOMOTIVE_FORM_FACTOR",
) )
) )
internal val layoutSwitchFingerprint = legacyFingerprint( internal val widthDpUIFingerprint = legacyFingerprint(
name = "layoutSwitchFingerprint", name = "widthDpUIFingerprint",
returnType = "I", returnType = "I",
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
parameters = listOf("L"),
opcodes = listOf( opcodes = listOf(
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_STATIC,
Opcode.MOVE_RESULT,
Opcode.IF_NEZ, Opcode.IF_NEZ,
Opcode.CONST_4, Opcode.CONST_4,
Opcode.RETURN, Opcode.RETURN,
@ -41,6 +37,11 @@ internal val layoutSwitchFingerprint = legacyFingerprint(
Opcode.CONST_4, Opcode.CONST_4,
Opcode.RETURN, Opcode.RETURN,
Opcode.CONST_4, Opcode.CONST_4,
Opcode.RETURN Opcode.RETURN,
),
literals = listOf(
480L,
600L,
720L
) )
) )

View File

@ -109,8 +109,7 @@ val navigationBarHookPatch = bytecodePatch(
hookNavigationButtonCreated = { extensionClassDescriptor -> hookNavigationButtonCreated = { extensionClassDescriptor ->
navigationBarHookCallbackFingerprint.methodOrThrow().addInstruction( navigationBarHookCallbackFingerprint.methodOrThrow().addInstruction(
0, 0,
"invoke-static { p0, p1 }, " + "invoke-static { p0, p1 }, $extensionClassDescriptor->navigationTabCreated" +
"$extensionClassDescriptor->navigationTabCreated" +
"(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V",
) )
} }

View File

@ -21,9 +21,9 @@ internal enum class PatchList(
"Bypass URL redirects", "Bypass URL redirects",
"Adds an option to bypass URL redirects and open the original URL directly." "Adds an option to bypass URL redirects and open the original URL directly."
), ),
CHANGE_LAYOUT( CHANGE_FORM_FACTOR(
"Change layout", "Change form factor",
"Adds an option to change the dp in order to use a tablet or phone layout." "Adds an option to change the UI appearance to a phone, tablet, or automotive device."
), ),
CHANGE_LIVE_RING_CLICK_ACTION( CHANGE_LIVE_RING_CLICK_ACTION(
"Change live ring click action", "Change live ring click action",

View File

@ -1,6 +1,7 @@
package app.revanced.patches.youtube.utils.playertype package app.revanced.patches.youtube.utils.playertype
import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer
import app.revanced.patches.youtube.utils.resourceid.toolbarContainerId
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
@ -9,6 +10,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val browseIdClassFingerprint = legacyFingerprint( internal val browseIdClassFingerprint = legacyFingerprint(
name = "browseIdClassFingerprint", name = "browseIdClassFingerprint",
@ -61,6 +63,34 @@ internal val searchQueryClassFingerprint = legacyFingerprint(
} }
) )
internal val toolbarLayoutFingerprint = legacyFingerprint(
name = "toolbarLayoutFingerprint",
literals = listOf(toolbarContainerId),
customFingerprint = { method, _ ->
method.name == "<init>" &&
indexOfMainCollapsingToolbarLayoutInstruction(method) >= 0
}
)
internal fun indexOfMainCollapsingToolbarLayoutInstruction(method: Method) =
method.indexOfFirstInstruction {
opcode == Opcode.CHECK_CAST &&
getReference<TypeReference>()?.type == "Lcom/google/android/apps/youtube/app/ui/actionbar/MainCollapsingToolbarLayout;"
}
/**
* Matches to https://android.googlesource.com/platform/frameworks/support/+/9eee6ba/v7/appcompat/src/android/support/v7/widget/Toolbar.java#963
*/
internal val appCompatToolbarBackButtonFingerprint = legacyFingerprint(
name = "appCompatToolbarBackButtonFingerprint",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "Landroid/graphics/drawable/Drawable;",
parameters = emptyList(),
customFingerprint = { _, classDef ->
classDef.type == "Landroid/support/v7/widget/Toolbar;"
},
)
internal val videoStateFingerprint = legacyFingerprint( internal val videoStateFingerprint = legacyFingerprint(
name = "videoStateFingerprint", name = "videoStateFingerprint",
returnType = "V", returnType = "V",

View File

@ -5,6 +5,7 @@ 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.patch.PatchException import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.shared.litho.addLithoFilter import app.revanced.patches.shared.litho.addLithoFilter
import app.revanced.patches.shared.litho.lithoFilterPatch import app.revanced.patches.shared.litho.lithoFilterPatch
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
@ -21,10 +22,13 @@ import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.indexOfFirstStringInstructionOrThrow import app.revanced.util.indexOfFirstStringInstructionOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
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.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR = private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
"$UTILS_PATH/PlayerTypeHookPatch;" "$UTILS_PATH/PlayerTypeHookPatch;"
@ -32,6 +36,9 @@ private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
private const val EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR = private const val EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR =
"$SHARED_PATH/RootView;" "$SHARED_PATH/RootView;"
private const val EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE =
"$SHARED_PATH/RootView${'$'}AppCompatToolbarPatchInterface;"
private const val FILTER_CLASS_DESCRIPTOR = private const val FILTER_CLASS_DESCRIPTOR =
"$COMPONENTS_PATH/RelatedVideoFilter;" "$COMPONENTS_PATH/RelatedVideoFilter;"
@ -165,6 +172,53 @@ val playerTypeHookPatch = bytecodePatch(
// endregion // endregion
// region patch for hook back button visibility
toolbarLayoutFingerprint.methodOrThrow().apply {
val index = indexOfMainCollapsingToolbarLayoutInstruction(this)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(
index + 1,
"invoke-static { v$register }, $EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR->setToolbar(Landroid/widget/FrameLayout;)V"
)
}
// Add interface for extensions code to call obfuscated methods.
appCompatToolbarBackButtonFingerprint.matchOrThrow().let {
it.classDef.apply {
interfaces.add(EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE)
val definingClass = type
val obfuscatedMethodName = it.originalMethod.name
val returnType = "Landroid/graphics/drawable/Drawable;"
methods.add(
ImmutableMethod(
definingClass,
"patch_getToolbarIcon",
listOf(),
returnType,
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
addInstructions(
0,
"""
invoke-virtual { p0 }, $definingClass->$obfuscatedMethodName()$returnType
move-result-object v0
return-object v0
"""
)
}
)
}
}
// endregion
addLithoFilter(FILTER_CLASS_DESCRIPTOR) addLithoFilter(FILTER_CLASS_DESCRIPTOR)
} }
} }

View File

@ -213,6 +213,8 @@ var tapBloomView = -1L
private set private set
var titleAnchor = -1L var titleAnchor = -1L
private set private set
var toolbarContainerId = -1L
private set
var toolTipContentView = -1L var toolTipContentView = -1L
private set private set
var totalTime = -1L var totalTime = -1L
@ -656,6 +658,10 @@ internal val sharedResourceIdPatch = resourcePatch(
ID, ID,
"title_anchor" "title_anchor"
] ]
toolbarContainerId = resourceMappings[
ID,
"toolbar_container"
]
toolTipContentView = resourceMappings[ toolTipContentView = resourceMappings[
LAYOUT, LAYOUT,
"tooltip_content_view" "tooltip_content_view"

View File

@ -22,19 +22,21 @@
<item>MIDDLE</item> <item>MIDDLE</item>
<item>END</item> <item>END</item>
</string-array> </string-array>
<string-array name="revanced_change_layout_entries"> <string-array name="revanced_change_form_factor_entries">
<item>@string/revanced_change_layout_entry_1</item> <item>@string/revanced_change_form_factor_entry_1</item>
<item>@string/revanced_change_layout_entry_2</item> <item>@string/revanced_change_form_factor_entry_2</item>
<item>@string/revanced_change_layout_entry_3</item> <item>@string/revanced_change_form_factor_entry_3</item>
<item>@string/revanced_change_layout_entry_4</item> <item>@string/revanced_change_form_factor_entry_4</item>
<item>@string/revanced_change_layout_entry_5</item> <item>@string/revanced_change_form_factor_entry_5</item>
<item>@string/revanced_change_form_factor_entry_6</item>
</string-array> </string-array>
<string-array name="revanced_change_layout_entry_values"> <string-array name="revanced_change_form_factor_entry_values">
<item>ORIGINAL</item> <item>DEFAULT</item>
<item>SMALL_FORM_FACTOR</item> <item>SMALL</item>
<item>SMALL_FORM_FACTOR_WIDTH_DP</item> <item>SMALL_WIDTH_DP</item>
<item>LARGE_FORM_FACTOR</item> <item>LARGE</item>
<item>LARGE_FORM_FACTOR_WIDTH_DP</item> <item>LARGE_WIDTH_DP</item>
<item>AUTOMOTIVE</item>
</string-array> </string-array>
<string-array name="revanced_change_start_page_entries"> <string-array name="revanced_change_start_page_entries">
<item>@string/revanced_change_start_page_entry_default</item> <item>@string/revanced_change_start_page_entry_default</item>

View File

@ -439,17 +439,27 @@ Limitation: Back button on the toolbar may not work."</string>
<string name="revanced_remove_viewer_discretion_dialog_summary">"Removes the viewer discretion dialog. <string name="revanced_remove_viewer_discretion_dialog_summary">"Removes the viewer discretion dialog.
This does not bypass the age restriction. It just accepts it automatically."</string> This does not bypass the age restriction. It just accepts it automatically."</string>
<string name="revanced_change_layout_title">Change layout</string>
<string name="revanced_change_layout_entry_1">Original</string>
<string name="revanced_change_layout_entry_2">Phone</string>
<string name="revanced_change_layout_entry_3">Phone (Max 480 dp)</string>
<string name="revanced_change_layout_entry_4">Tablet</string>
<string name="revanced_change_layout_entry_5">Tablet (Min 600 dp)</string>
<string name="revanced_change_live_ring_click_action_title">Change live ring click action</string> <string name="revanced_change_live_ring_click_action_title">Change live ring click action</string>
<string name="revanced_change_live_ring_click_action_summary_on">"Channel opens when the live ring is clicked. <string name="revanced_change_live_ring_click_action_summary_on">"Channel opens when the live ring is clicked.
Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open."</string> Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open."</string>
<string name="revanced_change_live_ring_click_action_summary_off">Live stream opens when the live ring is clicked.</string> <string name="revanced_change_live_ring_click_action_summary_off">Live stream opens when the live ring is clicked.</string>
<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">Phone (Max 480 dp)</string>
<string name="revanced_change_form_factor_entry_4">Tablet</string>
<string name="revanced_change_form_factor_entry_5">Tablet (Min 600 dp)</string>
<string name="revanced_change_form_factor_entry_6">Automotive</string>
<string name="revanced_change_form_factor_user_dialog_message">"Changes include:
Tablet layout
• Community posts are hidden.
Automotive layout
• Shorts open in the regular player.
• Feed is organized by topics and channels.
• Video description cannot be opened when 'Spoof streaming data' is turned off."</string>
<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>
<string name="revanced_spoof_app_version_summary_off">Version not spoofed</string> <string name="revanced_spoof_app_version_summary_off">Version not spoofed</string>

View File

@ -269,12 +269,12 @@
<!-- PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS <!-- PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS
<PreferenceCategory android:title="@string/revanced_preference_category_experimental_flag" android:layout="@layout/revanced_settings_preferences_category"/>PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS --> <PreferenceCategory android:title="@string/revanced_preference_category_experimental_flag" android:layout="@layout/revanced_settings_preferences_category"/>PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS -->
<!-- SETTINGS: CHANGE_LAYOUT
<ListPreference android:entries="@array/revanced_change_layout_entries" android:title="@string/revanced_change_layout_title" android:key="revanced_change_layout" android:entryValues="@array/revanced_change_layout_entry_values" />SETTINGS: CHANGE_LAYOUT -->
<!-- SETTINGS: CHANGE_LIVE_RING_CLICK_ACTION <!-- SETTINGS: CHANGE_LIVE_RING_CLICK_ACTION
<SwitchPreference android:title="@string/revanced_change_live_ring_click_action_title" android:key="revanced_change_live_ring_click_action" android:summaryOn="@string/revanced_change_live_ring_click_action_summary_on" android:summaryOff="@string/revanced_change_live_ring_click_action_summary_off" />SETTINGS: CHANGE_LIVE_RING_CLICK_ACTION --> <SwitchPreference android:title="@string/revanced_change_live_ring_click_action_title" android:key="revanced_change_live_ring_click_action" android:summaryOn="@string/revanced_change_live_ring_click_action_summary_on" android:summaryOff="@string/revanced_change_live_ring_click_action_summary_off" />SETTINGS: CHANGE_LIVE_RING_CLICK_ACTION -->
<!-- SETTINGS: CHANGE_FORM_FACTOR
<ListPreference android:entries="@array/revanced_change_form_factor_entries" android:title="@string/revanced_change_form_factor_title" android:key="revanced_change_form_factor" android:entryValues="@array/revanced_change_form_factor_entry_values" />SETTINGS: CHANGE_FORM_FACTOR -->
<!-- SETTINGS: SPOOF_APP_VERSION <!-- SETTINGS: SPOOF_APP_VERSION
<SwitchPreference android:title="@string/revanced_spoof_app_version_title" android:key="revanced_spoof_app_version" android:summaryOn="@string/revanced_spoof_app_version_summary_on" android:summaryOff="@string/revanced_spoof_app_version_summary_off" /> <SwitchPreference android:title="@string/revanced_spoof_app_version_title" android:key="revanced_spoof_app_version" android:summaryOn="@string/revanced_spoof_app_version_summary_on" android:summaryOff="@string/revanced_spoof_app_version_summary_off" />
<app.revanced.extension.shared.settings.preference.WideListPreference android:title="@string/revanced_spoof_app_version_target_entry_title" android:key="revanced_spoof_app_version_target" android:entries="@array/revanced_spoof_app_version_target_entries" android:entryValues="@array/revanced_spoof_app_version_target_entry_values" /> <app.revanced.extension.shared.settings.preference.WideListPreference android:title="@string/revanced_spoof_app_version_target_entry_title" android:key="revanced_spoof_app_version_target" android:entries="@array/revanced_spoof_app_version_target_entries" android:entryValues="@array/revanced_spoof_app_version_target_entry_values" />
@ -910,7 +910,7 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/revanced_preference_screen_general_title" android:layout="@layout/revanced_settings_preferences_category"> <PreferenceCategory android:title="@string/revanced_preference_screen_general_title" android:layout="@layout/revanced_settings_preferences_category">
<Preference android:title="Change layout" android:summary="@string/revanced_patches_excluded" android:selectable="false"/> <Preference android:title="Change form factor" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Change live ring click action" android:summary="@string/revanced_patches_excluded" android:selectable="false"/> <Preference android:title="Change live ring click action" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Change start page" android:summary="@string/revanced_patches_excluded" android:selectable="false"/> <Preference android:title="Change start page" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>
<Preference android:title="Disable forced auto audio tracks" android:summary="@string/revanced_patches_excluded" android:selectable="false"/> <Preference android:title="Disable forced auto audio tracks" android:summary="@string/revanced_patches_excluded" android:selectable="false"/>