Merge remote-tracking branch 'origin/feat/save_brightness' into feat/save_brightness

This commit is contained in:
MarcaDian 2024-04-10 23:51:14 +03:00
commit ffd2acc6da
No known key found for this signature in database
GPG Key ID: 904EF10755E7C016
14 changed files with 391 additions and 180 deletions

View File

@ -1,3 +1,43 @@
# [1.8.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.4...v1.8.0-dev.5) (2024-04-10)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Do not clip compact text when not using English ([eeaeb49](https://github.com/ReVanced/revanced-integrations/commit/eeaeb49f2a562d2690dae184153c303a5b1c4368))
# [1.8.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.3...v1.8.0-dev.4) (2024-04-10)
### Bug Fixes
* **YouTube - Hide Shorts components:** Correctly hide Shorts if navigation tab is changed using device back button ([#611](https://github.com/ReVanced/revanced-integrations/issues/611)) ([ffc3437](https://github.com/ReVanced/revanced-integrations/commit/ffc3437843c24af255d2a0dda9930d2843cac4b6))
# [1.8.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.2...v1.8.0-dev.3) (2024-04-09)
### Bug Fixes
* **YouTube - Settings:** Do not show a toast if migrating old unknown settings ([f2e15a2](https://github.com/ReVanced/revanced-integrations/commit/f2e15a2e1ff59ae7780cfbd366e5165f4e2b191d))
# [1.8.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.8.0-dev.1...v1.8.0-dev.2) (2024-04-07)
### Bug Fixes
* **YouTube - Hide layout components:** Do not hide playlist shelf in library ([c5d38a7](https://github.com/ReVanced/revanced-integrations/commit/c5d38a7e0791ebb8fe59397fff959cc94e0a7aed))
# [1.8.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.4...v1.8.0-dev.1) (2024-04-06)
### Bug Fixes
* **YouTube - GmsCore support:** Prompt to disable battery optimizations, if not done already ([#601](https://github.com/ReVanced/revanced-integrations/issues/601)) ([c5c9de5](https://github.com/ReVanced/revanced-integrations/commit/c5c9de500d8f1268799e55c31c446bfe8336f79a))
### Features
* **YouTube - Hide layout components:** Add option to hide horizontal shelves ([#598](https://github.com/ReVanced/revanced-integrations/issues/598)) ([fedace0](https://github.com/ReVanced/revanced-integrations/commit/fedace02fd5c443ef37dcf77253438b041f4c3f9))
## [1.7.1-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.7.1-dev.3...v1.7.1-dev.4) (2024-04-04)

View File

@ -1,21 +1,25 @@
package app.revanced.integrations.shared;
import static app.revanced.integrations.shared.StringRef.str;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.RequiresApi;
import java.net.MalformedURLException;
import java.net.URL;
import static app.revanced.integrations.shared.StringRef.str;
/**
* @noinspection unused
*/
@ -45,26 +49,19 @@ public class GmsCoreSupport {
System.exit(0);
}
private static void showToastOrDialog(Context context, String toastMessageKey, String dialogMessageKey, String link) {
if (!(context instanceof Activity)) {
// Context is for the application and cannot show a dialog using it.
Utils.showToastLong(str(toastMessageKey));
open(link);
return;
}
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageKey))
.setPositiveButton(str("gms_core_dialog_ok_button_text"), (dialog, id) -> {
open(link);
})
// Manually allow using the back button to dismiss the dialog with the back button,
// if troubleshooting and somehow the GmsCore verification checks always fail.
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
// Allow using back button to skip the action, just in case the check can never be satisfied.
.setCancelable(true)
.show();
}, 100);
@ -74,47 +71,62 @@ public class GmsCoreSupport {
* Injection point.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public static void checkGmsCore(Context context) {
public static void checkGmsCore(Activity context) {
try {
// Verify GmsCore is installed.
try {
PackageManager manager = context.getPackageManager();
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printDebug(() -> "GmsCore was not found");
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before the dialog can display.
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
open(getGmsCoreDownload());
return;
}
// Check if GmsCore is whitelisted from battery optimizations.
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (!powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME)) {
Logger.printDebug(() -> "GmsCore is not whitelisted from battery optimizations");
showToastOrDialog(context,
"gms_core_toast_not_whitelisted_message",
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
DONT_KILL_MY_APP_LINK);
return;
}
// Check if GmsCore is running in the background.
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
if (client == null) {
Logger.printDebug(() -> "GmsCore is not running in the background");
showToastOrDialog(context,
"gms_core_toast_not_whitelisted_message",
Logger.printInfo(() -> "GmsCore is not running in the background");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
DONT_KILL_MY_APP_LINK);
"gms_core_dialog_open_website_text",
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
return;
}
}
// Check if GmsCore is whitelisted from battery optimizations.
if (batteryOptimizationsEnabled(context)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
activity.startActivityForResult(intent, 0);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}
private static String getGmsCoreDownload() {
final var vendorGroupId = getGmsCoreVendorGroupId();
//noinspection SwitchStatementWithTooFewBranches

View File

@ -224,6 +224,7 @@ public abstract class Setting<T> {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
}
Object newValue = setting.get();
final Object migratedValue;
if (setting instanceof BooleanSetting) {
@ -238,13 +239,17 @@ public abstract class Setting<T> {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
// Remove otherwise it'll show a toast on every launch
oldPrefs.preferences.edit().remove(settingKey).apply();
return;
}
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
if (migratedValue.equals(newValue)) {
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
return; // Old value is already equal to the new setting value.
}
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
//noinspection unchecked
setting.save(migratedValue);

View File

@ -1,19 +1,15 @@
package app.revanced.integrations.youtube.patches;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.settings.Settings.*;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
import android.net.Uri;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.EnumSetting;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.shared.NavigationBar;
import app.revanced.integrations.youtube.shared.PlayerType;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlResponseInfo;
import org.chromium.net.impl.CronetUrlRequest;
@ -26,13 +22,12 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_HOME;
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY;
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER;
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH;
import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.NavigationBar;
import app.revanced.integrations.youtube.shared.PlayerType;
/**
* Alternative YouTube thumbnails.
@ -134,11 +129,6 @@ public final class AlternativeThumbnailsPatch {
*/
private static volatile long timeToResumeDeArrowAPICalls;
/**
* Used only for debug logging.
*/
private static volatile EnumSetting<ThumbnailOption> currentOptionSetting;
static {
dearrowApiUri = validateSettings();
final int port = dearrowApiUri.getPort();
@ -162,23 +152,38 @@ public final class AlternativeThumbnailsPatch {
return apiUri;
}
private static EnumSetting<ThumbnailOption> optionSettingForCurrentNavigation() {
private static ThumbnailOption optionSettingForCurrentNavigation() {
// Must check player type first, as search bar can be active behind the player.
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
return ALT_THUMBNAIL_PLAYER;
return ALT_THUMBNAIL_PLAYER.get();
}
// Must check second, as search can be from any tab.
if (NavigationBar.isSearchBarActive()) {
return ALT_THUMBNAIL_SEARCH;
return ALT_THUMBNAIL_SEARCH.get();
}
if (NavigationButton.HOME.isSelected()) {
return ALT_THUMBNAIL_HOME;
// Avoid checking which navigation button is selected, if all other settings are the same.
ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get();
ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get();
ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get();
if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) {
return homeOption; // All are the same option.
}
if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) {
return ALT_THUMBNAIL_SUBSCRIPTIONS;
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
// Unknown tab, treat as the home tab;
return homeOption;
}
if (selectedNavButton == NavigationButton.HOME) {
return homeOption;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) {
return subscriptionsOption;
}
// A library tab variant is active.
return ALT_THUMBNAIL_LIBRARY;
return libraryOption;
}
/**
@ -256,14 +261,7 @@ public final class AlternativeThumbnailsPatch {
*/
public static String overrideImageURL(String originalUrl) {
try {
EnumSetting<ThumbnailOption> optionSetting = optionSettingForCurrentNavigation();
ThumbnailOption option = optionSetting.get();
if (BaseSettings.DEBUG.get()) {
if (currentOptionSetting != optionSetting) {
currentOptionSetting = optionSetting;
Logger.printDebug(() -> "Changed to setting: " + optionSetting.key);
}
}
ThumbnailOption option = optionSettingForCurrentNavigation();
if (option == ThumbnailOption.ORIGINAL) {
return originalUrl;

View File

@ -1,29 +0,0 @@
package app.revanced.integrations.youtube.patches;
import android.view.View;
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Utils;
@SuppressWarnings("unused")
public class HideBreakingNewsPatch {
/**
* When spoofing to app versions 17.31.00 and older, the watch history preview bar uses
* the same layout components as the breaking news shelf.
*
* Breaking news does not appear to be present in these older versions anyways.
*/
private static final boolean isSpoofingOldVersionWithHorizontalCardListWatchHistory =
SpoofAppVersionPatch.isSpoofingToLessThan("18.01.00");
/**
* Injection point.
*/
public static void hideBreakingNews(View view) {
if (!Settings.HIDE_BREAKING_NEWS.get()
|| isSpoofingOldVersionWithHorizontalCardListWatchHistory) return;
Utils.hideViewByLayoutParams(view);
}
}

View File

@ -176,12 +176,6 @@ public class ReturnYouTubeDislikePatch {
textView.removeTextChangedListener(oldUiTextWatcher);
textView.addTextChangedListener(oldUiTextWatcher);
/**
* If the patch is changed to include the dislikes button as a parameter to this method,
* then if the button is already selected the dislikes could be adjusted using
* {@link ReturnYouTubeDislike#setUserVote(Vote)}
*/
updateOldUIDislikesTextView();
} catch (Exception ex) {
@ -314,19 +308,25 @@ public class ReturnYouTubeDislikePatch {
*/
public static float onRollingNumberMeasured(String text, float measuredTextWidth) {
try {
if (Settings.RYD_ENABLED.get() && !Settings.RYD_COMPACT_LAYOUT.get()) {
if (Settings.RYD_ENABLED.get()) {
if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) {
// +1 pixel is needed for some foreign languages that measure
// the text different from what is used for layout (Greek in particular).
// Probably a bug in Android, but who knows.
// Single line mode is also used as an additional fix for this issue.
return measuredTextWidth + ReturnYouTubeDislike.leftSeparatorBounds.right
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels + 1;
if (Settings.RYD_COMPACT_LAYOUT.get()) {
return measuredTextWidth + 1;
}
return measuredTextWidth + 1
+ ReturnYouTubeDislike.leftSeparatorBounds.right
+ ReturnYouTubeDislike.leftSeparatorShapePaddingPixels;
}
}
} catch (Exception ex) {
Logger.printException(() -> "onRollingNumberMeasured failure", ex);
}
return measuredTextWidth;
}
@ -344,10 +344,12 @@ public class ReturnYouTubeDislikePatch {
} else {
view.setCompoundDrawables(separator, null, null, null);
}
// Disliking can cause the span to grow in size, which is ok and is laid out correctly,
// but if the user then removes their dislike the layout will not adjust to the new shorter width.
// Use a center alignment to take up any extra space.
view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
// Single line mode does not clip words if the span is larger than the view bounds.
// The styled span applied to the view should always have the same bounds,
// but use this feature just in case the measurements are somehow off by a few pixels.

View File

@ -112,37 +112,35 @@ final class KeywordContentFilter extends Filter {
private volatile ByteTrieSearch bufferSearch;
private static void logNavigationState(String state) {
// Enable locally to debug filtering. Default off to reduce log spam.
final boolean LOG_NAVIGATION_STATE = false;
// noinspection ConstantValue
if (LOG_NAVIGATION_STATE) {
Logger.printDebug(() -> "Navigation state: " + state);
}
}
private static boolean hideKeywordSettingIsActive() {
// Must check player type first, as search bar can be active behind the player.
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
// For now, consider the under video results the same as the home feed.
logNavigationState("Player active");
return Settings.HIDE_KEYWORD_CONTENT_HOME.get();
}
// Must check second, as search can be from any tab.
if (NavigationBar.isSearchBarActive()) {
logNavigationState("Search");
return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get();
}
if (NavigationButton.HOME.isSelected()) {
logNavigationState("Home tab");
return Settings.HIDE_KEYWORD_CONTENT_HOME.get();
// Avoid checking navigation button status if all other settings are off.
final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get();
final boolean hideSubscriptions = Settings.HIDE_SUBSCRIPTIONS_BUTTON.get();
if (!hideHome && !hideSubscriptions) {
return false;
}
if (NavigationButton.SUBSCRIPTIONS.isSelected()) {
logNavigationState("Subscription tab");
return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get();
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home.
}
if (selectedNavButton == NavigationButton.HOME) {
return hideHome;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) {
return hideSubscriptions;
}
// User is in the Library or Notifications tab.
logNavigationState("Ignored tab");
return false;
}

View File

@ -1,15 +1,17 @@
package app.revanced.integrations.youtube.patches.components;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
import android.os.Build;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.NavigationBar;
import app.revanced.integrations.youtube.shared.PlayerType;
@ -35,6 +37,7 @@ public final class LayoutComponentsFilter extends Filter {
private final StringFilterGroup compactChannelBarInner;
private final StringFilterGroup compactChannelBarInnerButton;
private final ByteArrayFilterGroup joinMembershipButton;
private final StringFilterGroup horizontalShelves;
static {
mixPlaylistsExceptions.addPatterns(
@ -43,7 +46,6 @@ public final class LayoutComponentsFilter extends Filter {
);
}
@RequiresApi(api = Build.VERSION_CODES.N)
public LayoutComponentsFilter() {
exceptions.addPatterns(
@ -237,6 +239,12 @@ public final class LayoutComponentsFilter extends Filter {
"endorsement_header_footer"
);
horizontalShelves = new StringFilterGroup(
Settings.HIDE_HORIZONTAL_SHELVES,
"horizontal_video_shelf.eml",
"horizontal_shelf.eml"
);
addPathCallbacks(
expandableMetadata,
inFeedSurvey,
@ -263,7 +271,8 @@ public final class LayoutComponentsFilter extends Filter {
timedReactions,
imageShelf,
channelMemberShelf,
forYouShelf
forYouShelf,
horizontalShelves
);
}
@ -274,12 +283,15 @@ public final class LayoutComponentsFilter extends Filter {
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
// The groups are excluded from the filter due to the exceptions list below.
// Filter them separately here.
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
{
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
@ -298,6 +310,14 @@ public final class LayoutComponentsFilter extends Filter {
// TODO: This also hides the feed Shorts shelf header
if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false;
if (matchedGroup == horizontalShelves) {
if (contentIndex == 0 && hideShelves()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
@ -346,4 +366,20 @@ public final class LayoutComponentsFilter extends Filter {
Utils.hideViewByLayoutParams(view);
}
}
private static boolean hideShelves() {
// If the player is opened while library is selected,
// then filter any recommendations below the player.
if (PlayerType.getCurrent().isMaximizedOrFullscreen()
// Or if the search is active while library is selected, then also filter.
|| NavigationBar.isSearchBarActive()) {
return true;
}
// Check navigation button last.
// Only filter if the library tab is not selected.
// This check is important as the shelf layout is used for the library tab playlists.
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab();
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.integrations.youtube.patches.components;
import static app.revanced.integrations.shared.Utils.hideViewUnderCondition;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton;
import android.view.View;
@ -224,16 +225,30 @@ public final class ShortsFilter extends Filter {
// For now, consider the under video results the same as the home feed.
return Settings.HIDE_SHORTS_HOME.get();
}
// Must check second, as search can be from any tab.
if (NavigationBar.isSearchBarActive()) {
return Settings.HIDE_SHORTS_SEARCH.get();
}
if (NavigationBar.NavigationButton.HOME.isSelected()) {
return Settings.HIDE_SHORTS_HOME.get();
// Avoid checking navigation button status if all other settings are off.
final boolean hideHome = Settings.HIDE_SHORTS_HOME.get();
final boolean hideSubscriptions = Settings.HIDE_SHORTS_SUBSCRIPTIONS.get();
if (!hideHome && !hideSubscriptions) {
return false;
}
if (NavigationBar.NavigationButton.SUBSCRIPTIONS.isSelected()) {
return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get();
NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton();
if (selectedNavButton == null) {
return hideHome; // Unknown tab, treat the same as home.
}
if (selectedNavButton == NavigationButton.HOME) {
return hideHome;
}
if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) {
return hideSubscriptions;
}
// User must be in the library tab. Don't hide the history or any playlists here.
return false;
}

View File

@ -55,6 +55,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE);
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
// Layout
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
@ -76,7 +77,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true);
public static final BooleanSetting HIDE_ARTIST_CARDS = new BooleanSetting("revanced_hide_artist_cards", FALSE);
public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true);
public static final BooleanSetting HIDE_BREAKING_NEWS = new BooleanSetting("revanced_hide_breaking_news", TRUE, true);
public static final BooleanSetting HIDE_HORIZONTAL_SHELVES = new BooleanSetting("revanced_hide_horizontal_shelves", TRUE);
public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE);
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE);

View File

@ -79,10 +79,9 @@ public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment {
shortsPreference = new SwitchPreference(context);
shortsPreference.setChecked(Settings.RYD_SHORTS.get());
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
String shortsSummary = str("revanced_ryd_shorts_summary_on",
ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
? ""
: "\n\n" + str("revanced_ryd_shorts_summary_disclaimer"));
String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER
? str("revanced_ryd_shorts_summary_on")
: str("revanced_ryd_shorts_summary_on_disclaimer");
shortsPreference.setSummaryOn(shortsSummary);
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {

View File

@ -100,10 +100,10 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment {
privateUserId.setEnabled(enabled);
// If the user has a private user id, then include a subtext that mentions not to share it.
String exportSummarySubText = SponsorBlockSettings.userHasSBPrivateId()
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
? str("revanced_sb_settings_ie_sum_warning")
: "";
importExport.setSummary(str("revanced_sb_settings_ie_sum", exportSummarySubText));
: str("revanced_sb_settings_ie_sum");
importExport.setSummary(importExportSummary);
apiUrl.setEnabled(enabled);
importExport.setEnabled(enabled);

View File

@ -2,21 +2,29 @@ package app.revanced.integrations.youtube.shared;
import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class NavigationBar {
//
// Search bar
//
private static volatile WeakReference<View> searchBarResultsRef = new WeakReference<>(null);
/**
@ -36,11 +44,101 @@ public final class NavigationBar {
return searchbarResults != null && searchbarResults.getParent() != null;
}
//
// Navigation bar buttons
//
/**
* Last YT navigation enum loaded. Not necessarily the active navigation tab.
* How long to wait for the set nav button latch to be released. Maximum wait time must
* be as small as possible while still allowing enough time for the nav bar to update.
*
* YT calls it's back button handlers out of order,
* and litho starts filtering before the navigation bar is updated.
*
* Fixing this situation and not needlessly waiting requires somehow
* detecting if a back button key-press will cause a tab change.
*
* Typically after pressing the back button, the time between the first litho event and
* when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time
* and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways.
*
* This issue can also be avoided on a patch by patch basis, by avoiding calls to
* {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary.
*/
private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 75;
/**
* Used as a workaround to fix the issue of YT calling back button handlers out of order.
* Used to hold calls to {@link NavigationButton#getSelectedNavigationButton()}
* until the current navigation button can be determined.
*
* Only used when the hardware back button is pressed.
*/
@Nullable
private static volatile String lastYTNavigationEnumName;
private static volatile CountDownLatch navButtonLatch;
/**
* Map of nav button layout views to Enum type.
* No synchronization is needed, and this is always accessed from the main thread.
*/
private static final Map<View, NavigationButton> viewToButtonMap = new WeakHashMap<>();
static {
// On app startup litho can start before the navigation bar is initialized.
// Force it to wait until the nav bar is updated.
createNavButtonLatch();
}
private static void createNavButtonLatch() {
navButtonLatch = new CountDownLatch(1);
}
private static void releaseNavButtonLatch() {
CountDownLatch latch = navButtonLatch;
if (latch != null) {
navButtonLatch = null;
latch.countDown();
}
}
private static void waitForNavButtonLatchIfNeeded() {
CountDownLatch latch = navButtonLatch;
if (latch == null) {
return;
}
if (Utils.isCurrentlyOnMainThread()) {
// The latch is released from the main thread, and waiting from the main thread will always timeout.
// This situation has only been observed when navigating out of a submenu and not changing tabs.
// and for that use case the nav bar does not change so it's safe to return here.
Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status.");
return;
}
try {
Logger.printDebug(() -> "Latch wait started");
if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) {
// Back button changed the navigation tab.
Logger.printDebug(() -> "Latch wait complete");
return;
}
// Timeout occurred, and a normal event when pressing the physical back button
// does not change navigation tabs.
releaseNavButtonLatch(); // Prevent other threads from waiting for no reason.
Logger.printDebug(() -> "Latch wait timed out");
} catch (InterruptedException ex) {
Logger.printException(() -> "Latch wait interrupted failure", ex); // Will never happen.
}
}
/**
* Last YT navigation enum loaded. Not necessarily the active navigation tab.
* Always accessed from the main thread.
*/
@Nullable
private static String lastYTNavigationEnumName;
/**
* Injection point.
@ -57,21 +155,16 @@ public final class NavigationBar {
public static void navigationTabLoaded(final View navigationButtonGroup) {
try {
String lastEnumName = lastYTNavigationEnumName;
for (NavigationButton button : NavigationButton.values()) {
if (button.ytEnumName.equals(lastEnumName)) {
ImageView imageView = Utils.getChildView((ViewGroup) navigationButtonGroup,
true, view -> view instanceof ImageView);
if (imageView != null) {
Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName);
button.imageViewRef = new WeakReference<>(imageView);
navigationTabCreatedCallback(button, navigationButtonGroup);
return;
}
Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName);
viewToButtonMap.put(navigationButtonGroup, button);
navigationTabCreatedCallback(button, navigationButtonGroup);
return;
}
}
// Log the unknown tab as exception level, only if debug is enabled.
// This is because unknown tabs do no harm, and it's only relevant to developers.
if (Settings.DEBUG.get()) {
@ -99,6 +192,46 @@ public final class NavigationBar {
}
}
/**
* Injection point.
*/
public static void navigationTabSelected(View navButtonImageView, boolean isSelected) {
try {
NavigationButton button = viewToButtonMap.get(navButtonImageView);
if (button == null) { // An unknown tab was selected.
// Show a toast only if debug mode is enabled.
if (BaseSettings.DEBUG.get()) {
Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView);
}
NavigationButton.selectedNavigationButton = null;
return;
}
if (isSelected) {
NavigationButton.selectedNavigationButton = button;
Logger.printDebug(() -> "Changed to navigation button: " + button);
// Release any threads waiting for the selected nav button.
releaseNavButtonLatch();
} else if (NavigationButton.selectedNavigationButton == button) {
NavigationButton.selectedNavigationButton = null;
Logger.printDebug(() -> "Navigated away from button: " + button);
}
} catch (Exception ex) {
Logger.printException(() -> "navigationTabSelected failure", ex);
}
}
/**
* Injection point.
*/
public static void onBackPressed(Activity activity) {
Logger.printDebug(() -> "Back button pressed");
createNavButtonLatch();
}
/** @noinspection EmptyMethod*/
private static void navigationTabCreatedCallback(NavigationButton button, View tabView) {
// Code is added during patching.
@ -109,8 +242,7 @@ public final class NavigationBar {
SHORTS("TAB_SHORTS"),
/**
* Create new video tab.
*
* {@link #isSelected()} always returns false, 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"),
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"),
@ -145,41 +277,43 @@ public final class NavigationBar {
// The hooked YT code does not use an enum, and a dummy name is used here.
LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME");
@Nullable
private static volatile NavigationButton selectedNavigationButton;
/**
* This will return null only if the currently selected tab is unknown.
* This scenario will only happen if the UI has different tabs due to an A/B user test
* or YT abruptly changes the navigation layout for some other reason.
*
* All code calling this method should handle a null return value.
*
* <b>Due to issues with how YT processes physical back button events,
* this patch uses workarounds that can cause this method to take up to 75ms
* if the device back button was recently pressed.</b>
*
* @return The active navigation tab.
* If the user is in the create new video UI, this returns NULL.
* If the user is in the upload video UI, this returns tab that is still visually
* selected on screen (whatever tab the user was on before tapping the upload button).
*/
@Nullable
public static NavigationButton getSelectedNavigationButton() {
for (NavigationButton button : values()) {
if (button.isSelected()) return button;
}
return null;
}
/**
* @return If the currently selected tab is a 'You' or library type.
* Covers all known app states including incognito mode and version spoofing.
*/
public static boolean libraryOrYouTabIsSelected() {
return LIBRARY_YOU.isSelected() || LIBRARY_PIVOT_UNKNOWN.isSelected()
|| LIBRARY_OLD_UI.isSelected() || LIBRARY_INCOGNITO.isSelected()
|| LIBRARY_LOGGED_OUT.isSelected();
waitForNavButtonLatchIfNeeded();
return selectedNavigationButton;
}
/**
* YouTube enum name for this tab.
*/
private final String ytEnumName;
private volatile WeakReference<ImageView> imageViewRef = new WeakReference<>(null);
NavigationButton(String ytEnumName) {
this.ytEnumName = ytEnumName;
}
public boolean isSelected() {
ImageView view = imageViewRef.get();
return view != null && view.isSelected();
public boolean isLibraryOrYouTab() {
return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN
|| this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO
|| this == LIBRARY_LOGGED_OUT;
}
}
}

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
android.useAndroidX = true
version = 1.7.1-dev.4
version = 1.8.0-dev.5