chore: Merge branch dev to main (#548)

This commit is contained in:
oSumAtrIX 2024-01-27 03:09:26 +01:00 committed by GitHub
commit 3100a7899c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
263 changed files with 5082 additions and 17269 deletions

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: gradle
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly

View File

@ -24,25 +24,24 @@ jobs:
persist-credentials: false
fetch-depth: 0
- name: Cache Node modules
uses: actions/cache@v3
with:
path: |
node_modules
key: npm-${{ hashFiles('package-lock.json') }}
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Setup Java
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
- name: Build with Gradle
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean
- name: Setup semantic-release
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Release

View File

@ -1,3 +1,60 @@
# [1.2.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.2.0-dev.2...v1.2.0-dev.3) (2024-01-27)
### Features
* Move strings to resources for localization ([#420](https://github.com/ReVanced/revanced-integrations/issues/420)) ([7ae10be](https://github.com/ReVanced/revanced-integrations/commit/7ae10be507244594adf6704975fdf4cfd797a96e))
* **YouTube - Spoof app version:** Add `18.09.39` to restore library tab ([#552](https://github.com/ReVanced/revanced-integrations/issues/552)) ([3bd48dc](https://github.com/ReVanced/revanced-integrations/commit/3bd48dca09094f58f68b8cfb6ff047fafa40b7e3))
# [1.2.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.2.0-dev.1...v1.2.0-dev.2) (2024-01-16)
### Features
* **YouTube:** Support versions `18.48.39`, `18.49.37`, `19.01.34` ([#547](https://github.com/ReVanced/revanced-integrations/issues/547)) ([eaaa6fb](https://github.com/ReVanced/revanced-integrations/commit/eaaa6fbd20630f15bfec7c57713ec353f4072ac2))
# [1.2.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.1.1-dev.5...v1.2.0-dev.1) (2024-01-10)
### Features
* **Tiktok - Playback speed:** Remember playback speed ([#543](https://github.com/ReVanced/revanced-integrations/issues/543)) ([21ced14](https://github.com/ReVanced/revanced-integrations/commit/21ced14791c6c26745ad88af5ce9d4970ad4c951))
## [1.1.1-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.1.1-dev.4...v1.1.1-dev.5) (2024-01-08)
### Bug Fixes
* **YouTube - DeArrow:** Correctly handle http status 304 ([3e380df](https://github.com/ReVanced/revanced-integrations/commit/3e380dfce27c6fbf68f44d67929a92ca069b1ba2))
## [1.1.1-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.1.1-dev.3...v1.1.1-dev.4) (2024-01-06)
### Bug Fixes
* **YouTube - Settings:** Correctly initialize default values ([752544b](https://github.com/ReVanced/revanced-integrations/commit/752544b9627c57c1044cc8e93b42aca32cdb8518))
## [1.1.1-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.1.1-dev.2...v1.1.1-dev.3) (2024-01-05)
### Bug Fixes
* **YouTube - Hide ads:** Do not leave screen at launch non interactable when hiding fullscreen ads ([fbdb490](https://github.com/ReVanced/revanced-integrations/commit/fbdb4908ea96a99b80ced99384b2dfdcc8fccd8a))
## [1.1.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.1.1-dev.1...v1.1.1-dev.2) (2024-01-02)
### Bug Fixes
* **YouTube - SponsorBlock:** Update categories after import JSON import ([211f954](https://github.com/ReVanced/revanced-integrations/commit/211f9542e8a725ca9cbfff9d3b42b4fc40734db3))
## [1.1.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.1.0...v1.1.1-dev.1) (2023-12-29)
### Bug Fixes
* **YouTube - Hide ads:** Fix dimmed screen at launch by not filtering fullscreen ads ([1a1f44d](https://github.com/ReVanced/revanced-integrations/commit/1a1f44d2355bb5e93eefca77b8df41211f6fff42))
# [1.1.0](https://github.com/ReVanced/revanced-integrations/compare/v1.0.0...v1.1.0) (2023-12-28)

View File

@ -1,4 +1,4 @@
package app.revanced.all.connectivity.wifi.spoof;
package app.revanced.integrations.all.connectivity.wifi.spoof;
import android.app.PendingIntent;
import android.content.Context;

View File

@ -1,4 +1,4 @@
package app.revanced.all.screencapture.removerestriction;
package app.revanced.integrations.all.screencapture.removerestriction;
import android.media.AudioAttributes;
import android.os.Build;

View File

@ -1,4 +1,4 @@
package app.revanced.all.screenshot.removerestriction;
package app.revanced.integrations.all.screenshot.removerestriction;
import android.view.Window;
import android.view.WindowManager;

View File

@ -1,10 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class AutoRepeatPatch {
//Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch
public static boolean shouldAutoRepeat() {
return SettingsEnum.AUTO_REPEAT.getBoolean();
}
}

View File

@ -1,16 +0,0 @@
package app.revanced.integrations.patches;
import android.content.Intent;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
@SuppressWarnings("unused")
public final class ChangeStartPagePatch {
public static void changeIntent(Intent intent) {
final var startPage = SettingsEnum.START_PAGE.getString();
if (startPage.isEmpty()) return;
LogHelper.printDebug(() -> "Changing start page to " + startPage);
intent.setAction("com.google.android.youtube.action." + startPage);
}
}

View File

@ -1,22 +0,0 @@
package app.revanced.integrations.patches;
import android.widget.ImageView;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class CustomPlayerOverlayOpacityPatch {
private static final int DEFAULT_OPACITY = (int) SettingsEnum.PLAYER_OVERLAY_OPACITY.defaultValue;
public static void changeOpacity(ImageView imageView) {
int opacity = SettingsEnum.PLAYER_OVERLAY_OPACITY.getInt();
if (opacity < 0 || opacity > 100) {
ReVancedUtils.showToastLong("Player overlay opacity must be between 0-100");
SettingsEnum.PLAYER_OVERLAY_OPACITY.saveValue(DEFAULT_OPACITY);
opacity = DEFAULT_OPACITY;
}
imageView.setImageAlpha((opacity * 255) / 100);
}
}

View File

@ -1,10 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
/** @noinspection unused*/
public final class DisableFullscreenAmbientModePatch {
public static boolean enableFullScreenAmbientMode() {
return !SettingsEnum.DISABLE_FULLSCREEN_AMBIENT_MODE.getBoolean();
}
}

View File

@ -1,12 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class DisableRollingNumberAnimationsPatch {
/**
* Injection point.
*/
public static boolean disableRollingNumberAnimations() {
return SettingsEnum.DISABLE_ROLLING_NUMBER_ANIMATIONS.getBoolean();
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public final class EnableTabletLayoutPatch {
public static boolean enableTabletLayout() {
return SettingsEnum.TABLET_LAYOUT.getBoolean();
}
}

View File

@ -1,10 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
public class FullscreenPanelsRemoverPatch {
public static int getFullscreenPanelsVisibility() {
return SettingsEnum.HIDE_FULLSCREEN_PANELS.getBoolean() ? View.GONE : View.VISIBLE;
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideAlbumCardsPatch {
public static void hideAlbumCard(View view) {
if (!SettingsEnum.HIDE_ALBUM_CARDS.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class HideAutoplayButtonPatch {
public static boolean isButtonShown() {
return !SettingsEnum.HIDE_AUTOPLAY_BUTTON.getBoolean();
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.integrations.patches;
import android.widget.ImageView;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
public class HideCaptionsButtonPatch {
//Used by app.revanced.patches.youtube.layout.hidecaptionsbutton.patch.HideCaptionsButtonPatch
public static void hideCaptionsButton(ImageView imageView) {
imageView.setVisibility(SettingsEnum.HIDE_CAPTIONS_BUTTON.getBoolean() ? ImageView.GONE : ImageView.VISIBLE);
}
}

View File

@ -1,14 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideCrowdfundingBoxPatch {
//Used by app.revanced.patches.youtube.layout.hidecrowdfundingbox.patch.HideCrowdfundingBoxPatch
public static void hideCrowdfundingBox(View view) {
if (!SettingsEnum.HIDE_CROWDFUNDING_BOX.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -1,25 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public final class HideFilterBarPatch {
public static int hideInFeed(final int height) {
if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_FEED.getBoolean()) return 0;
return height;
}
public static void hideInRelatedVideos(final View chipView) {
if (!SettingsEnum.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(chipView);
}
public static int hideInSearch(final int height) {
if (SettingsEnum.HIDE_FILTER_BAR_FEED_IN_SEARCH.getBoolean()) return 0;
return height;
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public final class HideFloatingMicrophoneButtonPatch {
public static boolean hideFloatingMicrophoneButton(final boolean original) {
return SettingsEnum.HIDE_FLOATING_MICROPHONE_BUTTON.getBoolean() || original;
}
}

View File

@ -1,12 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class HideGetPremiumPatch {
/**
* Injection point.
*/
public static boolean hideGetPremiumView() {
return SettingsEnum.HIDE_GET_PREMIUM.getBoolean();
}
}

View File

@ -1,15 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
public class HideInfoCardsPatch {
public static void hideInfoCardsIncognito(View view) {
if (!SettingsEnum.HIDE_INFO_CARDS.getBoolean()) return;
view.setVisibility(View.GONE);
}
public static boolean hideInfoCardsMethodCall() {
return SettingsEnum.HIDE_INFO_CARDS.getBoolean();
}
}

View File

@ -1,13 +0,0 @@
package app.revanced.integrations.patches;
import android.view.View;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.ReVancedUtils;
public class HideLoadMoreButtonPatch {
public static void hideLoadMoreButton(View view){
if(!SettingsEnum.HIDE_LOAD_MORE_BUTTON.getBoolean()) return;
ReVancedUtils.hideViewByLayoutParams(view);
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class HideSeekbarPatch {
public static boolean hideSeekbar() {
return SettingsEnum.HIDE_SEEKBAR.getBoolean();
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class HideTimestampPatch {
public static boolean hideTimestamp() {
return SettingsEnum.HIDE_TIMESTAMP.getBoolean();
}
}

View File

@ -1,10 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
@SuppressWarnings("unused")
public final class RestoreOldSeekbarThumbnailsPatch {
public static boolean useFullscreenSeekbarThumbnails() {
return !SettingsEnum.RESTORE_OLD_SEEKBAR_THUMBNAILS.getBoolean();
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public final class SeekbarTappingPatch {
public static boolean seekbarTappingEnabled() {
return SettingsEnum.SEEKBAR_TAPPING.getBoolean();
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public final class SlideToSeekPatch {
public static boolean isSlideToSeekDisabled() {
return !SettingsEnum.SLIDE_TO_SEEK.getBoolean();
}
}

View File

@ -1,12 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class TabletMiniPlayerOverridePatch {
public static boolean getTabletMiniPlayerOverride(boolean original) {
if (SettingsEnum.USE_TABLET_MINIPLAYER.getBoolean())
return true;
return original;
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public final class WideSearchbarPatch {
public static boolean enableWideSearchbar() {
return SettingsEnum.WIDE_SEARCHBAR.getBoolean();
}
}

View File

@ -1,9 +0,0 @@
package app.revanced.integrations.patches;
import app.revanced.integrations.settings.SettingsEnum;
public class ZoomHapticsPatch {
public static boolean shouldVibrate() {
return !SettingsEnum.DISABLE_ZOOM_HAPTICS.getBoolean();
}
}

View File

@ -1,39 +0,0 @@
package app.revanced.integrations.patches.playback.speed;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
public final class RememberPlaybackSpeedPatch {
/**
* Injection point.
*/
public static void newVideoStarted(Object ignoredPlayerController) {
LogHelper.printDebug(() -> "newVideoStarted");
VideoInformation.overridePlaybackSpeed(SettingsEnum.PLAYBACK_SPEED_DEFAULT.getFloat());
}
/**
* Injection point.
* Called when user selects a playback speed.
*
* @param playbackSpeed The playback speed the user selected
*/
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
if (SettingsEnum.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.getBoolean()) {
SettingsEnum.PLAYBACK_SPEED_DEFAULT.saveValue(playbackSpeed);
ReVancedUtils.showToastLong("Changed default speed to: " + playbackSpeed + "x");
}
}
/**
* Injection point.
* Overrides the video speed. Called after video loads, and immediately after user selects a different playback speed
*/
public static float getPlaybackSpeedOverride() {
return VideoInformation.getPlaybackSpeed();
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.reddit.patches;
package app.revanced.integrations.reddit.patches;
import com.reddit.domain.model.ILink;

View File

@ -1,762 +0,0 @@
package app.revanced.integrations.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.sponsorblock.SponsorBlockSettings;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.integrations.utils.StringRef;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*;
import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE;
import static app.revanced.integrations.settings.SharedPrefCategory.SPONSOR_BLOCK;
import static app.revanced.integrations.utils.StringRef.str;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
public enum SettingsEnum {
// External downloader
EXTERNAL_DOWNLOADER("revanced_external_downloader", BOOLEAN, FALSE),
EXTERNAL_DOWNLOADER_PACKAGE_NAME("revanced_external_downloader_name", STRING,
"org.schabi.newpipe" /* NewPipe */, parents(EXTERNAL_DOWNLOADER)),
// Copy video URL
COPY_VIDEO_URL("revanced_copy_video_url", BOOLEAN, FALSE),
COPY_VIDEO_URL_TIMESTAMP("revanced_copy_video_url_timestamp", BOOLEAN, TRUE),
// Video
HDR_AUTO_BRIGHTNESS("revanced_hdr_auto_brightness", BOOLEAN, TRUE),
@Deprecated SHOW_OLD_VIDEO_QUALITY_MENU("revanced_show_old_video_quality_menu", BOOLEAN, TRUE),
RESTORE_OLD_VIDEO_QUALITY_MENU("revanced_restore_old_video_quality_menu", BOOLEAN, TRUE),
REMEMBER_VIDEO_QUALITY_LAST_SELECTED("revanced_remember_video_quality_last_selected", BOOLEAN, TRUE),
VIDEO_QUALITY_DEFAULT_WIFI("revanced_video_quality_default_wifi", INTEGER, -2),
VIDEO_QUALITY_DEFAULT_MOBILE("revanced_video_quality_default_mobile", INTEGER, -2),
REMEMBER_PLAYBACK_SPEED_LAST_SELECTED("revanced_remember_playback_speed_last_selected", BOOLEAN, TRUE),
PLAYBACK_SPEED_DEFAULT("revanced_playback_speed_default", FLOAT, 1.0f),
CUSTOM_PLAYBACK_SPEEDS("revanced_custom_playback_speeds", STRING,
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true),
// Ads
HIDE_FULLSCREEN_ADS("revanced_hide_fullscreen_ads", BOOLEAN, TRUE),
HIDE_BUTTONED_ADS("revanced_hide_buttoned_ads", BOOLEAN, TRUE),
HIDE_GENERAL_ADS("revanced_hide_general_ads", BOOLEAN, TRUE),
HIDE_GET_PREMIUM("revanced_hide_get_premium", BOOLEAN, TRUE),
HIDE_HIDE_LATEST_POSTS("revanced_hide_latest_posts_ads", BOOLEAN, TRUE),
HIDE_MERCHANDISE_BANNERS("revanced_hide_merchandise_banners", BOOLEAN, TRUE),
HIDE_PAID_CONTENT("revanced_hide_paid_content_ads", BOOLEAN, TRUE),
HIDE_PRODUCTS_BANNER("revanced_hide_products_banner", BOOLEAN, TRUE),
HIDE_SHOPPING_LINKS("revanced_hide_shopping_links", BOOLEAN, TRUE),
HIDE_SELF_SPONSOR("revanced_hide_self_sponsor_ads", BOOLEAN, TRUE),
HIDE_VIDEO_ADS("revanced_hide_video_ads", BOOLEAN, TRUE, true),
HIDE_WEB_SEARCH_RESULTS("revanced_hide_web_search_results", BOOLEAN, TRUE),
// Layout
ALT_THUMBNAIL_STILLS("revanced_alt_thumbnail_stills", BOOLEAN, FALSE),
ALT_THUMBNAIL_STILLS_TIME("revanced_alt_thumbnail_stills_time", INTEGER, 2, parents(ALT_THUMBNAIL_STILLS)),
ALT_THUMBNAIL_STILLS_FAST("revanced_alt_thumbnail_stills_fast", BOOLEAN, FALSE, parents(ALT_THUMBNAIL_STILLS)),
ALT_THUMBNAIL_DEARROW("revanced_alt_thumbnail_dearrow", BOOLEAN, false),
ALT_THUMBNAIL_DEARROW_API_URL("revanced_alt_thumbnail_dearrow_api_url", STRING,
"https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parents(ALT_THUMBNAIL_DEARROW)),
ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST("revanced_alt_thumbnail_dearrow_connection_toast", BOOLEAN, TRUE, parents(ALT_THUMBNAIL_DEARROW)),
CUSTOM_FILTER("revanced_custom_filter", BOOLEAN, FALSE),
CUSTOM_FILTER_STRINGS("revanced_custom_filter_strings", STRING, "", true, parents(CUSTOM_FILTER)),
DISABLE_FULLSCREEN_AMBIENT_MODE("revanced_disable_fullscreen_ambient_mode", BOOLEAN, TRUE, true),
DISABLE_RESUMING_SHORTS_PLAYER("revanced_disable_resuming_shorts_player", BOOLEAN, FALSE),
DISABLE_ROLLING_NUMBER_ANIMATIONS("revanced_disable_rolling_number_animations", BOOLEAN, FALSE),
DISABLE_SUGGESTED_VIDEO_END_SCREEN("revanced_disable_suggested_video_end_screen", BOOLEAN, TRUE),
GRADIENT_LOADING_SCREEN("revanced_gradient_loading_screen", BOOLEAN, FALSE),
HIDE_ALBUM_CARDS("revanced_hide_album_cards", BOOLEAN, FALSE, true),
HIDE_ARTIST_CARDS("revanced_hide_artist_cards", BOOLEAN, FALSE),
HIDE_AUTOPLAY_BUTTON("revanced_hide_autoplay_button", BOOLEAN, TRUE, true),
HIDE_BREAKING_NEWS("revanced_hide_breaking_news", BOOLEAN, TRUE, true),
HIDE_CAPTIONS_BUTTON("revanced_hide_captions_button", BOOLEAN, FALSE),
HIDE_CAST_BUTTON("revanced_hide_cast_button", BOOLEAN, TRUE, true),
HIDE_CHANNEL_BAR("revanced_hide_channel_bar", BOOLEAN, FALSE),
HIDE_CHANNEL_MEMBER_SHELF("revanced_hide_channel_member_shelf", BOOLEAN, TRUE),
HIDE_CHIPS_SHELF("revanced_hide_chips_shelf", BOOLEAN, TRUE),
HIDE_COMMENTS_SECTION("revanced_hide_comments_section", BOOLEAN, FALSE, true),
HIDE_COMMUNITY_GUIDELINES("revanced_hide_community_guidelines", BOOLEAN, TRUE),
HIDE_COMMUNITY_POSTS("revanced_hide_community_posts", BOOLEAN, FALSE),
HIDE_COMPACT_BANNER("revanced_hide_compact_banner", BOOLEAN, TRUE),
HIDE_CREATE_BUTTON("revanced_hide_create_button", BOOLEAN, TRUE, true),
HIDE_CROWDFUNDING_BOX("revanced_hide_crowdfunding_box", BOOLEAN, FALSE, true),
HIDE_EMAIL_ADDRESS("revanced_hide_email_address", BOOLEAN, FALSE),
HIDE_EMERGENCY_BOX("revanced_hide_emergency_box", BOOLEAN, TRUE),
HIDE_ENDSCREEN_CARDS("revanced_hide_endscreen_cards", BOOLEAN, TRUE),
HIDE_EXPANDABLE_CHIP("revanced_hide_expandable_chip", BOOLEAN, TRUE),
HIDE_FEED_SURVEY("revanced_hide_feed_survey", BOOLEAN, TRUE),
HIDE_FILTER_BAR_FEED_IN_FEED("revanced_hide_filter_bar_feed_in_feed", BOOLEAN, FALSE, true),
HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS("revanced_hide_filter_bar_feed_in_related_videos", BOOLEAN, FALSE, true),
HIDE_FILTER_BAR_FEED_IN_SEARCH("revanced_hide_filter_bar_feed_in_search", BOOLEAN, FALSE, true),
HIDE_FLOATING_MICROPHONE_BUTTON("revanced_hide_floating_microphone_button", BOOLEAN, TRUE, true),
HIDE_FULLSCREEN_PANELS("revanced_hide_fullscreen_panels", BOOLEAN, TRUE, true),
HIDE_GRAY_SEPARATOR("revanced_hide_gray_separator", BOOLEAN, TRUE),
HIDE_HIDE_CHANNEL_GUIDELINES("revanced_hide_channel_guidelines", BOOLEAN, TRUE),
HIDE_HIDE_INFO_PANELS("revanced_hide_info_panels", BOOLEAN, TRUE),
HIDE_HOME_BUTTON("revanced_hide_home_button", BOOLEAN, FALSE, true),
HIDE_IMAGE_SHELF("revanced_hide_image_shelf", BOOLEAN, TRUE),
HIDE_INFO_CARDS("revanced_hide_info_cards", BOOLEAN, TRUE),
HIDE_JOIN_MEMBERSHIP_BUTTON("revanced_hide_join_membership_button", BOOLEAN, TRUE),
HIDE_LOAD_MORE_BUTTON("revanced_hide_load_more_button", BOOLEAN, TRUE, true),
HIDE_MEDICAL_PANELS("revanced_hide_medical_panels", BOOLEAN, TRUE),
HIDE_MIX_PLAYLISTS("revanced_hide_mix_playlists", BOOLEAN, TRUE),
HIDE_MOVIES_SECTION("revanced_hide_movies_section", BOOLEAN, TRUE),
HIDE_NOTIFY_ME_BUTTON("revanced_hide_notify_me_button", BOOLEAN, TRUE),
HIDE_PLAYER_BUTTONS("revanced_hide_player_buttons", BOOLEAN, FALSE),
HIDE_PREVIEW_COMMENT("revanced_hide_preview_comment", BOOLEAN, FALSE, true),
HIDE_QUICK_ACTIONS("revanced_hide_quick_actions", BOOLEAN, FALSE),
HIDE_RELATED_VIDEOS("revanced_hide_related_videos", BOOLEAN, FALSE),
HIDE_SEARCH_RESULT_SHELF_HEADER("revanced_hide_search_result_shelf_header", BOOLEAN, FALSE),
HIDE_SHORTS_BUTTON("revanced_hide_shorts_button", BOOLEAN, TRUE, true),
HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES("revanced_hide_subscribers_community_guidelines", BOOLEAN, TRUE),
HIDE_SUBSCRIPTIONS_BUTTON("revanced_hide_subscriptions_button", BOOLEAN, FALSE, true),
HIDE_TIMED_REACTIONS("revanced_hide_timed_reactions", BOOLEAN, TRUE),
HIDE_TIMESTAMP("revanced_hide_timestamp", BOOLEAN, FALSE),
@Deprecated HIDE_VIDEO_WATERMARK("revanced_hide_video_watermark", BOOLEAN, TRUE),
HIDE_VIDEO_CHANNEL_WATERMARK("revanced_hide_channel_watermark", BOOLEAN, TRUE),
HIDE_FOR_YOU_SHELF("revanced_hide_for_you_shelf", BOOLEAN, TRUE),
HIDE_VIDEO_QUALITY_MENU_FOOTER("revanced_hide_video_quality_menu_footer", BOOLEAN, TRUE),
HIDE_SEARCH_RESULT_RECOMMENDATIONS("revanced_hide_search_result_recommendations", BOOLEAN, TRUE),
PLAYER_OVERLAY_OPACITY("revanced_player_overlay_opacity", INTEGER, 100, true),
PLAYER_POPUP_PANELS("revanced_hide_player_popup_panels", BOOLEAN, FALSE),
SPOOF_APP_VERSION("revanced_spoof_app_version", BOOLEAN, FALSE, true, "revanced_spoof_app_version_user_dialog_message"),
SPOOF_APP_VERSION_TARGET("revanced_spoof_app_version_target", STRING, "17.08.35", true, parents(SPOOF_APP_VERSION)),
SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON("revanced_switch_create_with_notifications_button", BOOLEAN, TRUE, true),
TABLET_LAYOUT("revanced_tablet_layout", BOOLEAN, FALSE, true, "revanced_tablet_layout_user_dialog_message"),
USE_TABLET_MINIPLAYER("revanced_tablet_miniplayer", BOOLEAN, FALSE, true),
WIDE_SEARCHBAR("revanced_wide_searchbar", BOOLEAN, FALSE, true),
START_PAGE("revanced_start_page", STRING, ""),
// Description
HIDE_CHAPTERS("revanced_hide_chapters", BOOLEAN, TRUE),
HIDE_INFO_CARDS_SECTION("revanced_hide_info_cards_section", BOOLEAN, TRUE),
HIDE_GAME_SECTION("revanced_hide_game_section", BOOLEAN, TRUE),
HIDE_MUSIC_SECTION("revanced_hide_music_section", BOOLEAN, TRUE),
HIDE_PODCAST_SECTION("revanced_hide_podcast_section", BOOLEAN, TRUE),
HIDE_TRANSCIPT_SECTION("revanced_hide_transcript_section", BOOLEAN, TRUE),
// Shorts
HIDE_SHORTS("revanced_hide_shorts", BOOLEAN, FALSE, true),
HIDE_SHORTS_JOIN_BUTTON("revanced_hide_shorts_join_button", BOOLEAN, TRUE),
HIDE_SHORTS_SUBSCRIBE_BUTTON("revanced_hide_shorts_subscribe_button", BOOLEAN, TRUE),
HIDE_SHORTS_SUBSCRIBE_BUTTON_PAUSED("revanced_hide_shorts_subscribe_button_paused", BOOLEAN, FALSE),
HIDE_SHORTS_THANKS_BUTTON("revanced_hide_shorts_thanks_button", BOOLEAN, TRUE),
HIDE_SHORTS_COMMENTS_BUTTON("revanced_hide_shorts_comments_button", BOOLEAN, FALSE),
HIDE_SHORTS_REMIX_BUTTON("revanced_hide_shorts_remix_button", BOOLEAN, TRUE),
HIDE_SHORTS_SHARE_BUTTON("revanced_hide_shorts_share_button", BOOLEAN, FALSE),
HIDE_SHORTS_INFO_PANEL("revanced_hide_shorts_info_panel", BOOLEAN, TRUE),
HIDE_SHORTS_SOUND_BUTTON("revanced_hide_shorts_sound_button", BOOLEAN, FALSE),
HIDE_SHORTS_CHANNEL_BAR("revanced_hide_shorts_channel_bar", BOOLEAN, FALSE),
HIDE_SHORTS_NAVIGATION_BAR("revanced_hide_shorts_navigation_bar", BOOLEAN, TRUE, true),
// Seekbar
@Deprecated ENABLE_OLD_SEEKBAR_THUMBNAILS("revanced_enable_old_seekbar_thumbnails", BOOLEAN, TRUE),
RESTORE_OLD_SEEKBAR_THUMBNAILS("revanced_restore_old_seekbar_thumbnails", BOOLEAN, TRUE),
HIDE_SEEKBAR("revanced_hide_seekbar", BOOLEAN, FALSE),
HIDE_SEEKBAR_THUMBNAIL("revanced_hide_seekbar_thumbnail", BOOLEAN, FALSE),
SEEKBAR_CUSTOM_COLOR("revanced_seekbar_custom_color", BOOLEAN, TRUE, true),
SEEKBAR_CUSTOM_COLOR_VALUE("revanced_seekbar_custom_color_value", STRING, "#FF0000", true, parents(SEEKBAR_CUSTOM_COLOR)),
// Action buttons
HIDE_LIKE_DISLIKE_BUTTON("revanced_hide_like_dislike_button", BOOLEAN, FALSE),
HIDE_LIVE_CHAT_BUTTON("revanced_hide_live_chat_button", BOOLEAN, FALSE),
HIDE_SHARE_BUTTON("revanced_hide_share_button", BOOLEAN, FALSE),
HIDE_REPORT_BUTTON("revanced_hide_report_button", BOOLEAN, FALSE),
HIDE_REMIX_BUTTON("revanced_hide_remix_button", BOOLEAN, TRUE),
HIDE_DOWNLOAD_BUTTON("revanced_hide_download_button", BOOLEAN, FALSE),
HIDE_THANKS_BUTTON("revanced_hide_thanks_button", BOOLEAN, TRUE),
HIDE_CLIP_BUTTON("revanced_hide_clip_button", BOOLEAN, TRUE),
HIDE_PLAYLIST_BUTTON("revanced_hide_playlist_button", BOOLEAN, FALSE),
HIDE_SHOP_BUTTON("revanced_hide_shop_button", BOOLEAN, TRUE),
// Player flyout menu items
HIDE_CAPTIONS_MENU("revanced_hide_player_flyout_captions", BOOLEAN, FALSE),
HIDE_ADDITIONAL_SETTINGS_MENU("revanced_hide_player_flyout_additional_settings", BOOLEAN, FALSE),
HIDE_LOOP_VIDEO_MENU("revanced_hide_player_flyout_loop_video", BOOLEAN, FALSE),
HIDE_AMBIENT_MODE_MENU("revanced_hide_player_flyout_ambient_mode", BOOLEAN, FALSE),
HIDE_REPORT_MENU("revanced_hide_player_flyout_report", BOOLEAN, TRUE),
HIDE_HELP_MENU("revanced_hide_player_flyout_help", BOOLEAN, TRUE),
HIDE_SPEED_MENU("revanced_hide_player_flyout_speed", BOOLEAN, FALSE),
HIDE_MORE_INFO_MENU("revanced_hide_player_flyout_more_info", BOOLEAN, TRUE),
HIDE_AUDIO_TRACK_MENU("revanced_hide_player_flyout_audio_track", BOOLEAN, FALSE),
HIDE_WATCH_IN_VR_MENU("revanced_hide_player_flyout_watch_in_vr", BOOLEAN, TRUE),
// Misc
AUTO_CAPTIONS("revanced_auto_captions", BOOLEAN, FALSE),
DISABLE_ZOOM_HAPTICS("revanced_disable_zoom_haptics", BOOLEAN, TRUE),
EXTERNAL_BROWSER("revanced_external_browser", BOOLEAN, TRUE, true),
AUTO_REPEAT("revanced_auto_repeat", BOOLEAN, FALSE),
SEEKBAR_TAPPING("revanced_seekbar_tapping", BOOLEAN, TRUE),
SLIDE_TO_SEEK("revanced_slide_to_seek", BOOLEAN, FALSE),
@Deprecated DISABLE_FINE_SCRUBBING_GESTURE("revanced_disable_fine_scrubbing_gesture", BOOLEAN, TRUE),
DISABLE_PRECISE_SEEKING_GESTURE("revanced_disable_precise_seeking_gesture", BOOLEAN, TRUE),
SPOOF_SIGNATURE("revanced_spoof_signature_verification_enabled", BOOLEAN, TRUE, true,
"revanced_spoof_signature_verification_enabled_user_dialog_message"),
SPOOF_SIGNATURE_IN_FEED("revanced_spoof_signature_in_feed_enabled", BOOLEAN, FALSE, false,
parents(SPOOF_SIGNATURE)),
SPOOF_STORYBOARD_RENDERER("revanced_spoof_storyboard", BOOLEAN, TRUE, true,
parents(SPOOF_SIGNATURE)),
SPOOF_DEVICE_DIMENSIONS("revanced_spoof_device_dimensions", BOOLEAN, FALSE, true),
BYPASS_URL_REDIRECTS("revanced_bypass_url_redirects", BOOLEAN, TRUE),
ANNOUNCEMENTS("revanced_announcements", BOOLEAN, TRUE),
ANNOUNCEMENT_CONSUMER("revanced_announcement_consumer", STRING, ""),
ANNOUNCEMENT_LAST_HASH("revanced_announcement_last_hash", STRING, ""),
REMOVE_TRACKING_QUERY_PARAMETER("revanced_remove_tracking_query_parameter", BOOLEAN, TRUE),
REMOVE_VIEWER_DISCRETION_DIALOG("revanced_remove_viewer_discretion_dialog", BOOLEAN, FALSE,
"revanced_remove_viewer_discretion_dialog_user_dialog_message"),
// Swipe controls
SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE),
SWIPE_VOLUME("revanced_swipe_volume", BOOLEAN, TRUE),
SWIPE_PRESS_TO_ENGAGE("revanced_swipe_press_to_engage", BOOLEAN, FALSE, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_HAPTIC_FEEDBACK("revanced_swipe_haptic_feedback", BOOLEAN, TRUE, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_MAGNITUDE_THRESHOLD("revanced_swipe_threshold", INTEGER, 30, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_BACKGROUND_ALPHA("revanced_swipe_overlay_background_alpha", INTEGER, 127, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_TEXT_SIZE("revanced_swipe_text_overlay_size", INTEGER, 22, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_OVERLAY_TIMEOUT("revanced_swipe_overlay_timeout", LONG, 500L, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
SWIPE_SAVE_AND_RESTORE_BRIGHTNESS("revanced_swipe_save_and_restore_brightness", BOOLEAN, TRUE, true,
parents(SWIPE_BRIGHTNESS, SWIPE_VOLUME)),
// Debugging
DEBUG("revanced_debug", BOOLEAN, FALSE),
DEBUG_STACKTRACE("revanced_debug_stacktrace", BOOLEAN, FALSE, parents(DEBUG)),
DEBUG_PROTOBUFFER("revanced_debug_protobuffer", BOOLEAN, FALSE, parents(DEBUG)),
DEBUG_TOAST_ON_ERROR("revanced_debug_toast_on_error", BOOLEAN, TRUE, "revanced_debug_toast_on_error_user_dialog_message"),
// ReturnYoutubeDislike
RYD_ENABLED("ryd_enabled", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE),
RYD_USER_ID("ryd_user_id", STRING, "", RETURN_YOUTUBE_DISLIKE),
RYD_SHORTS("ryd_shorts", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_DISLIKE_PERCENTAGE("ryd_dislike_percentage", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_COMPACT_LAYOUT("ryd_compact_layout", BOOLEAN, FALSE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
RYD_TOAST_ON_CONNECTION_ERROR("ryd_toast_on_connection_error", BOOLEAN, TRUE, RETURN_YOUTUBE_DISLIKE, parents(RYD_ENABLED)),
// SponsorBlock
SB_ENABLED("sb_enabled", BOOLEAN, TRUE, SPONSOR_BLOCK),
SB_PRIVATE_USER_ID("sb_private_user_id_Do_Not_Share", STRING, "", SPONSOR_BLOCK), /** Do not use directly, instead use {@link SponsorBlockSettings} */
DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING("uuid", STRING, "", SPONSOR_BLOCK), // Delete sometime in 2024
SB_CREATE_NEW_SEGMENT_STEP("sb_create_new_segment_step", INTEGER, 150, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_VOTING_BUTTON("sb_voting_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_CREATE_NEW_SEGMENT("sb_create_new_segment", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_COMPACT_SKIP_BUTTON("sb_compact_skip_button", BOOLEAN, FALSE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_AUTO_HIDE_SKIP_BUTTON("sb_auto_hide_skip_button", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TOAST_ON_SKIP("sb_toast_on_skip", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TOAST_ON_CONNECTION_ERROR("sb_toast_on_connection_error", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_TRACK_SKIP_COUNT("sb_track_skip_count", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_SEGMENT_MIN_DURATION("sb_min_segment_duration", FLOAT, 0F, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_VIDEO_LENGTH_WITHOUT_SEGMENTS("sb_video_length_without_segments", BOOLEAN, TRUE, SPONSOR_BLOCK, parents(SB_ENABLED)),
SB_API_URL("sb_api_url", STRING, "https://sponsor.ajay.app", SPONSOR_BLOCK),
SB_USER_IS_VIP("sb_user_is_vip", BOOLEAN, FALSE, SPONSOR_BLOCK),
// SB settings not exported
SB_LAST_VIP_CHECK("sb_last_vip_check", LONG, 0L, SPONSOR_BLOCK),
SB_HIDE_EXPORT_WARNING("sb_hide_export_warning", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_SEEN_GUIDELINES("sb_seen_guidelines", BOOLEAN, FALSE, SPONSOR_BLOCK),
SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS("sb_local_time_saved_number_segments", INTEGER, 0, SPONSOR_BLOCK),
SB_LOCAL_TIME_SAVED_MILLISECONDS("sb_local_time_saved_milliseconds", LONG, 0L, SPONSOR_BLOCK);
private static SettingsEnum[] parents(SettingsEnum... parents) {
return parents;
}
@NonNull
public final String path;
@NonNull
public final Object defaultValue;
@NonNull
public final SharedPrefCategory sharedPref;
@NonNull
public final ReturnType returnType;
/**
* If the app should be rebooted, if this setting is changed
*/
public final boolean rebootApp;
/**
* Set of boolean parent settings.
* If any of the parents are enabled, then this setting is available to configure.
*
* For example: {@link #DEBUG_STACKTRACE} is non-functional and cannot be configured,
* unless it's parent {@link #DEBUG} is enabled.
*
* Declaration is not needed for items that do not appear in the ReVanced Settings UI.
*/
@Nullable
private final SettingsEnum[] parents;
/**
* Confirmation message to display, if the user tries to change the setting from the default value.
* Can only be used for {@link ReturnType#BOOLEAN} setting types.
*/
@Nullable
public final StringRef userDialogMessage;
// Must be volatile, as some settings are read/write from different threads.
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
@NonNull
private volatile Object value;
SettingsEnum(String path, ReturnType returnType, Object defaultValue) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
String userDialogMessage) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, false, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, String userDialogMessage) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue,
boolean rebootApp, String userDialogMessage, SettingsEnum[] parents) {
this(path, returnType, defaultValue, SharedPrefCategory.YOUTUBE, rebootApp, userDialogMessage, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName) {
this(path, returnType, defaultValue, prefName, false, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
boolean rebootApp) {
this(path, returnType, defaultValue, prefName, rebootApp, null, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
String userDialogMessage) {
this(path, returnType, defaultValue, prefName, false, userDialogMessage, null);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
SettingsEnum[] parents) {
this(path, returnType, defaultValue, prefName, false, null, parents);
}
SettingsEnum(String path, ReturnType returnType, Object defaultValue, SharedPrefCategory prefName,
boolean rebootApp, @Nullable String userDialogMessage, @Nullable SettingsEnum[] parents) {
this.path = Objects.requireNonNull(path);
this.returnType = Objects.requireNonNull(returnType);
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
this.sharedPref = Objects.requireNonNull(prefName);
this.rebootApp = rebootApp;
if (userDialogMessage == null) {
this.userDialogMessage = null;
} else {
if (returnType != ReturnType.BOOLEAN) {
throw new IllegalArgumentException("must be Boolean type: " + path);
}
this.userDialogMessage = new StringRef(userDialogMessage);
}
this.parents = parents;
if (parents != null) {
for (SettingsEnum parent : parents) {
if (parent.returnType != ReturnType.BOOLEAN) {
throw new IllegalArgumentException("parent must be Boolean type: " + parent);
}
}
}
}
private static final Map<String, SettingsEnum> pathToSetting = new HashMap<>(2* values().length);
static {
loadAllSettings();
for (SettingsEnum setting : values()) {
pathToSetting.put(setting.path, setting);
}
}
@Nullable
public static SettingsEnum settingFromPath(@NonNull String str) {
return pathToSetting.get(str);
}
private static void loadAllSettings() {
for (SettingsEnum setting : values()) {
setting.load();
}
// region Migration
migrateOldSettingToNew(HIDE_VIDEO_WATERMARK, HIDE_VIDEO_CHANNEL_WATERMARK);
migrateOldSettingToNew(DISABLE_FINE_SCRUBBING_GESTURE, DISABLE_PRECISE_SEEKING_GESTURE);
migrateOldSettingToNew(SHOW_OLD_VIDEO_QUALITY_MENU, RESTORE_OLD_VIDEO_QUALITY_MENU);
migrateOldSettingToNew(ENABLE_OLD_SEEKBAR_THUMBNAILS, RESTORE_OLD_SEEKBAR_THUMBNAILS);
// Do _not_ delete this SB private user id migration property until sometime in 2024.
// This is the only setting that cannot be reconfigured if lost,
// and more time should be given for users who rarely upgrade.
migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID);
// This migration may need to remain here for a while.
// Older online guides will still reference using commas,
// and this code will automatically convert anything the user enters to newline format,
// and also migrate any imported older settings that using commas.
String componentsToFilter = SettingsEnum.CUSTOM_FILTER_STRINGS.getString();
if (componentsToFilter.contains(",")) {
LogHelper.printInfo(() -> "Migrating custom filter strings to new line format");
SettingsEnum.CUSTOM_FILTER_STRINGS.saveValue(componentsToFilter.replace(",", "\n"));
}
// endregion
}
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
private static void migrateOldSettingToNew(SettingsEnum oldSetting, SettingsEnum newSetting) {
if (!oldSetting.isSetToDefault()) {
LogHelper.printInfo(() -> "Migrating old setting of '" + oldSetting.value
+ "' from: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.saveValue(oldSetting.value);
oldSetting.resetToDefault();
}
}
private void load() {
switch (returnType) {
case BOOLEAN:
value = sharedPref.getBoolean(path, (boolean) defaultValue);
break;
case INTEGER:
value = sharedPref.getIntegerString(path, (Integer) defaultValue);
break;
case LONG:
value = sharedPref.getLongString(path, (Long) defaultValue);
break;
case FLOAT:
value = sharedPref.getFloatString(path, (Float) defaultValue);
break;
case STRING:
value = sharedPref.getString(path, (String) defaultValue);
break;
default:
throw new IllegalStateException(name());
}
}
/**
* Sets, but does _not_ persistently save the value.
* <p>
* This intentionally is a static method, to deter accidental usage
* when {@link #saveValue(Object)} was intended.
* <p>
* This method is only to be used by the Settings preference code.
*/
public static void setValue(@NonNull SettingsEnum setting, @NonNull String newValue) {
Objects.requireNonNull(newValue);
switch (setting.returnType) {
case BOOLEAN:
setting.value = Boolean.valueOf(newValue);
break;
case INTEGER:
setting.value = Integer.valueOf(newValue);
break;
case LONG:
setting.value = Long.valueOf(newValue);
break;
case FLOAT:
setting.value = Float.valueOf(newValue);
break;
case STRING:
setting.value = newValue;
break;
default:
throw new IllegalStateException(setting.name());
}
}
/**
* This method is only to be used by the Settings preference code.
*/
public static void setValue(@NonNull SettingsEnum setting, @NonNull Boolean newValue) {
setting.returnType.validate(newValue);
setting.value = newValue;
}
/**
* Sets the value, and persistently saves it.
*/
public void saveValue(@NonNull Object newValue) {
returnType.validate(newValue);
value = newValue; // Must set before saving to preferences (otherwise importing fails to update UI correctly).
switch (returnType) {
case BOOLEAN:
sharedPref.saveBoolean(path, (boolean) newValue);
break;
case INTEGER:
sharedPref.saveIntegerString(path, (Integer) newValue);
break;
case LONG:
sharedPref.saveLongString(path, (Long) newValue);
break;
case FLOAT:
sharedPref.saveFloatString(path, (Float) newValue);
break;
case STRING:
sharedPref.saveString(path, (String) newValue);
break;
default:
throw new IllegalStateException(name());
}
}
/**
* Identical to calling {@link #saveValue(Object)} using {@link #defaultValue}.
*/
public void resetToDefault() {
saveValue(defaultValue);
}
/**
* @return if this setting can be configured and used.
* <p>
* Not to be confused with {@link #getBoolean()}
*/
public boolean isAvailable() {
if (parents == null) {
return true;
}
for (SettingsEnum parent : parents) {
if (parent.getBoolean()) return true;
}
return false;
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
}
public boolean getBoolean() {
return (Boolean) value;
}
public int getInt() {
return (Integer) value;
}
public long getLong() {
return (Long) value;
}
public float getFloat() {
return (Float) value;
}
@NonNull
public String getString() {
return (String) value;
}
/**
* @return the value of this setting as as generic object type.
*/
@NonNull
public Object getObjectValue() {
return value;
}
/**
* This could be yet another field,
* for now use a simple switch statement since this method is not used outside this class.
*/
private boolean includeWithImportExport() {
switch (this) {
case RYD_USER_ID: // Not useful to export, no reason to include it.
case ANNOUNCEMENT_CONSUMER: // Not useful to export, no reason to include it.
case SB_LAST_VIP_CHECK:
case SB_HIDE_EXPORT_WARNING:
case SB_SEEN_GUIDELINES:
return false;
}
return true;
}
// Begin import / export
/**
* If a setting path has this prefix, then remove it before importing/exporting.
*/
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
/**
* The path, minus any 'revanced' prefix to keep json concise.
*/
private String getImportExportKey() {
if (path.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
return path.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
}
return path;
}
private static SettingsEnum[] valuesSortedForExport() {
SettingsEnum[] sorted = values();
Arrays.sort(sorted, (SettingsEnum o1, SettingsEnum o2) -> {
// Organize SponsorBlock settings last.
final boolean o1IsSb = o1.sharedPref == SPONSOR_BLOCK;
final boolean o2IsSb = o2.sharedPref == SPONSOR_BLOCK;
if (o1IsSb != o2IsSb) {
return o1IsSb ? 1 : -1;
}
return o1.path.compareTo(o2.path);
});
return sorted;
}
@NonNull
public static String exportJSON(@Nullable Context alertDialogContext) {
try {
JSONObject json = new JSONObject();
for (SettingsEnum setting : valuesSortedForExport()) {
String importExportKey = setting.getImportExportKey();
if (json.has(importExportKey)) {
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
}
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
if (setting.includeWithImportExport() && (!setting.isSetToDefault() | exportDefaultValues)) {
json.put(importExportKey, setting.getObjectValue());
}
}
SponsorBlockSettings.exportCategoriesToFlatJson(alertDialogContext, json);
if (json.length() == 0) {
return "";
}
String export = json.toString(0);
// Remove the outer JSON braces to make the output more compact,
// and leave less chance of the user forgetting to copy it
return export.substring(2, export.length() - 2);
} catch (JSONException e) {
LogHelper.printException(() -> "Export failure", e); // should never happen
return "";
}
}
/**
* @return if any settings that require a reboot were changed.
*/
public static boolean importJSON(@NonNull String settingsJsonString) {
try {
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
}
JSONObject json = new JSONObject(settingsJsonString);
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
for (SettingsEnum setting : values()) {
String key = setting.getImportExportKey();
if (json.has(key)) {
Object value;
switch (setting.returnType) {
case BOOLEAN:
value = json.getBoolean(key);
break;
case INTEGER:
value = json.getInt(key);
break;
case LONG:
value = json.getLong(key);
break;
case FLOAT:
value = (float) json.getDouble(key);
break;
case STRING:
value = json.getString(key);
break;
default:
throw new IllegalStateException();
}
if (!setting.getObjectValue().equals(value)) {
rebootSettingChanged |= setting.rebootApp;
setting.saveValue(value);
}
numberOfSettingsImported++;
} else if (setting.includeWithImportExport() && !setting.isSetToDefault()) {
LogHelper.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp;
setting.resetToDefault();
}
}
numberOfSettingsImported += SponsorBlockSettings.importCategoriesFromFlatJson(json);
ReVancedUtils.showToastLong(numberOfSettingsImported == 0
? str("revanced_settings_import_reset")
: str("revanced_settings_import_success", numberOfSettingsImported));
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
ReVancedUtils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
LogHelper.printInfo(() -> "", ex);
} catch (Exception ex) {
LogHelper.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
}
return false;
}
// End import / export
public enum ReturnType {
BOOLEAN,
INTEGER,
LONG,
FLOAT,
STRING;
public void validate(@Nullable Object obj) throws IllegalArgumentException {
if (!matches(obj)) {
throw new IllegalArgumentException("'" + obj + "' does not match:" + this);
}
}
public boolean matches(@Nullable Object obj) {
switch (this) {
case BOOLEAN:
return obj instanceof Boolean;
case INTEGER:
return obj instanceof Integer;
case LONG:
return obj instanceof Long;
case FLOAT:
return obj instanceof Float;
case STRING:
return obj instanceof String;
}
return false;
}
}
}

View File

@ -1,196 +0,0 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.SwitchPreference;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.settings.SharedPrefCategory;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import app.revanced.shared.settings.SettingsUtils;
public class ReVancedSettingsFragment extends PreferenceFragment {
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
*/
static boolean settingImportInProgress;
static void showRestartDialog(@NonNull Context contxt) {
String positiveButton = str("in_app_update_restart_button");
new AlertDialog.Builder(contxt).setMessage(str("pref_refresh_config"))
.setPositiveButton(positiveButton, (dialog, id) -> {
SettingsUtils.restartApp(contxt);
})
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.show();
}
/**
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
*/
private boolean showingUserDialogMessage;
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
SettingsEnum setting = SettingsEnum.settingFromPath(str);
if (setting == null) {
return;
}
Preference pref = findPreference(str);
LogHelper.printDebug(() -> setting.name() + ": " + " setting value:" + setting.getObjectValue() + " pref:" + pref);
if (pref == null) {
return;
}
if (pref instanceof SwitchPreference) {
SwitchPreference switchPref = (SwitchPreference) pref;
if (settingImportInProgress) {
switchPref.setChecked(setting.getBoolean());
} else {
SettingsEnum.setValue(setting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference) {
EditTextPreference editPreference = (EditTextPreference) pref;
if (settingImportInProgress) {
editPreference.getEditText().setText(setting.getObjectValue().toString());
} else {
SettingsEnum.setValue(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
if (settingImportInProgress) {
listPref.setValue(setting.getObjectValue().toString());
} else {
SettingsEnum.setValue(setting, listPref.getValue());
}
updateListPreferenceSummary((ListPreference) pref, setting);
} else {
LogHelper.printException(() -> "Setting cannot be handled: " + pref.getClass() + " " + pref);
return;
}
enableDisablePreferences();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation(getContext(), (SwitchPreference) pref, setting);
} else if (setting.rebootApp) {
showRestartDialog(getContext());
}
}
} catch (Exception ex) {
LogHelper.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
};
@SuppressLint("ResourceType")
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
try {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(SharedPrefCategory.YOUTUBE.prefName);
addPreferencesFromResource(ReVancedUtils.getResourceIdentifier("revanced_prefs", "xml"));
enableDisablePreferences();
// if the preference was included, then initialize it based on the available playback speed
Preference defaultSpeedPreference = findPreference(SettingsEnum.PLAYBACK_SPEED_DEFAULT.path);
if (defaultSpeedPreference instanceof ListPreference) {
CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference);
}
// Set current value from SettingsEnum
for (SettingsEnum setting : SettingsEnum.values()) {
Preference preference = findPreference(setting.path);
if (preference instanceof SwitchPreference) {
((SwitchPreference) preference).setChecked(setting.getBoolean());
} else if (preference instanceof EditTextPreference) {
((EditTextPreference) preference).setText(setting.getObjectValue().toString());
} else if (preference instanceof ListPreference) {
updateListPreferenceSummary((ListPreference) preference, setting);
}
}
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
} catch (Exception ex) {
LogHelper.printException(() -> "onActivityCreated() failure", ex);
}
}
@Override // android.preference.PreferenceFragment, android.app.Fragment
public void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
private void enableDisablePreferences() {
for (SettingsEnum setting : SettingsEnum.values()) {
Preference preference = this.findPreference(setting.path);
if (preference != null) {
preference.setEnabled(setting.isAvailable());
}
}
}
/**
* Sets summary text to the currently selected list option.
*/
private void updateListPreferenceSummary(ListPreference listPreference, SettingsEnum setting) {
String objectStringValue = setting.getObjectValue().toString();
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
if (entryIndex >= 0) {
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
listPreference.setValue(objectStringValue);
} else {
// Value is not an available option.
// User manually edited import data, or options changed and current selection is no longer available.
// Still show the value in the summary so it's clear that something is selected.
listPreference.setSummary(objectStringValue);
}
}
private void showSettingUserDialogConfirmation(@NonNull Context context, SwitchPreference switchPref, SettingsEnum setting) {
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(str("revanced_settings_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
Boolean defaultBooleanValue = (Boolean) setting.defaultValue;
SettingsEnum.setValue(setting, defaultBooleanValue);
switchPref.setChecked(defaultBooleanValue);
})
.setOnDismissListener(dialog -> {
showingUserDialogMessage = false;
})
.setCancelable(false)
.show();
}
}

View File

@ -1,230 +0,0 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.settings.SharedPrefCategory;
public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment {
/**
* If dislikes are shown on Shorts.
*/
private SwitchPreference shortsPreference;
/**
* If dislikes are shown as percentage.
*/
private SwitchPreference percentagePreference;
/**
* If segmented like/dislike button uses smaller compact layout.
*/
private SwitchPreference compactLayoutPreference;
/**
* If segmented like/dislike button uses smaller compact layout.
*/
private SwitchPreference toastOnRYDNotAvailable;
private void updateUIState() {
shortsPreference.setEnabled(SettingsEnum.RYD_SHORTS.isAvailable());
percentagePreference.setEnabled(SettingsEnum.RYD_DISLIKE_PERCENTAGE.isAvailable());
compactLayoutPreference.setEnabled(SettingsEnum.RYD_COMPACT_LAYOUT.isAvailable());
toastOnRYDNotAvailable.setEnabled(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable());
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getPreferenceManager().setSharedPreferencesName(SharedPrefCategory.RETURN_YOUTUBE_DISLIKE.prefName);
Activity context = this.getActivity();
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
SwitchPreference enabledPreference = new SwitchPreference(context);
enabledPreference.setChecked(SettingsEnum.RYD_ENABLED.getBoolean());
enabledPreference.setTitle(str("revanced_ryd_enable_title"));
enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on"));
enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off"));
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
final boolean rydIsEnabled = (Boolean) newValue;
SettingsEnum.RYD_ENABLED.saveValue(rydIsEnabled);
ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled);
updateUIState();
return true;
});
preferenceScreen.addPreference(enabledPreference);
shortsPreference = new SwitchPreference(context);
shortsPreference.setChecked(SettingsEnum.RYD_SHORTS.getBoolean());
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"));
shortsPreference.setSummaryOn(shortsSummary);
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_SHORTS.saveValue(newValue);
updateUIState();
return true;
});
preferenceScreen.addPreference(shortsPreference);
percentagePreference = new SwitchPreference(context);
percentagePreference.setChecked(SettingsEnum.RYD_DISLIKE_PERCENTAGE.getBoolean());
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on"));
percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off"));
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_DISLIKE_PERCENTAGE.saveValue(newValue);
ReturnYouTubeDislike.clearAllUICaches();
updateUIState();
return true;
});
preferenceScreen.addPreference(percentagePreference);
compactLayoutPreference = new SwitchPreference(context);
compactLayoutPreference.setChecked(SettingsEnum.RYD_COMPACT_LAYOUT.getBoolean());
compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on"));
compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off"));
compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_COMPACT_LAYOUT.saveValue(newValue);
ReturnYouTubeDislike.clearAllUICaches();
updateUIState();
return true;
});
preferenceScreen.addPreference(compactLayoutPreference);
toastOnRYDNotAvailable = new SwitchPreference(context);
toastOnRYDNotAvailable.setChecked(SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean());
toastOnRYDNotAvailable.setTitle(str("ryd_toast_on_connection_error_title"));
toastOnRYDNotAvailable.setSummaryOn(str("ryd_toast_on_connection_error_summary_on"));
toastOnRYDNotAvailable.setSummaryOff(str("ryd_toast_on_connection_error_summary_off"));
toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> {
SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.saveValue(newValue);
updateUIState();
return true;
});
preferenceScreen.addPreference(toastOnRYDNotAvailable);
updateUIState();
// About category
PreferenceCategory aboutCategory = new PreferenceCategory(context);
aboutCategory.setTitle(str("revanced_ryd_about"));
preferenceScreen.addPreference(aboutCategory);
// ReturnYouTubeDislike Website
Preference aboutWebsitePreference = new Preference(context);
aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title"));
aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary"));
aboutWebsitePreference.setOnPreferenceClickListener(pref -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://returnyoutubedislike.com"));
pref.getContext().startActivity(i);
return false;
});
preferenceScreen.addPreference(aboutWebsitePreference);
// RYD API connection statistics
if (SettingsEnum.DEBUG.getBoolean()) {
PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding
preferenceScreen.addPreference(emptyCategory);
PreferenceCategory statisticsCategory = new PreferenceCategory(context);
statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title"));
preferenceScreen.addPreference(statisticsCategory);
Preference statisticPreference;
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title"));
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()));
preferenceScreen.addPreference(statisticPreference);
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title"));
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()));
preferenceScreen.addPreference(statisticPreference);
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title"));
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()));
preferenceScreen.addPreference(statisticPreference);
String fetchCallTimeWaitingLastSummary;
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
} else {
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
}
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title"));
statisticPreference.setSummary(fetchCallTimeWaitingLastSummary);
preferenceScreen.addPreference(statisticPreference);
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title"));
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"));
preferenceScreen.addPreference(statisticPreference);
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title"));
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"));
preferenceScreen.addPreference(statisticPreference);
statisticPreference = new Preference(context);
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title"));
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"));
preferenceScreen.addPreference(statisticPreference);
}
}
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
if (value == 0) {
return str(summaryStringZeroKey);
}
return String.format(str(summaryStringOneOrMoreKey), value);
}
private static String createMillisecondStringFromNumber(long number) {
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
}
}

View File

@ -1,16 +1,19 @@
package app.revanced.integrations.utils;
package app.revanced.integrations.shared;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_STACKTRACE;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
import app.revanced.integrations.settings.SettingsEnum;
public class LogHelper {
public class Logger {
/**
* Log messages using lambdas.
@ -52,13 +55,13 @@ public class LogHelper {
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link SettingsEnum#DEBUG} is enabled.
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message) {
if (SettingsEnum.DEBUG.getBoolean()) {
if (DEBUG.get()) {
var messageString = message.buildMessageString();
if (SettingsEnum.DEBUG_STACKTRACE.getBoolean()) {
if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(messageString);
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
@ -125,12 +128,21 @@ public class LogHelper {
} else {
Log.e(logMessage, messageString, ex);
}
if (SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean()) {
if (DEBUG_TOAST_ON_ERROR.get()) {
String toastMessageToDisplay = (userToastMessage != null)
? userToastMessage
: outerClassSimpleName + ": " + messageString;
ReVancedUtils.showToastLong(toastMessageToDisplay);
Utils.showToastLong(toastMessageToDisplay);
}
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#context} may not be initialized.
* Always logs even if Debugging is not enabled.
* Normally this method should not be used.
*/
public static void initializationError(@NonNull Class<?> callingClass, @NonNull String message, @Nullable Exception ex) {
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.integrations.utils;
package app.revanced.integrations.shared;
import android.content.Context;
import android.content.res.Resources;
@ -102,7 +102,7 @@ public class StringRef {
public String toString() {
if (!resolved) {
if (resources == null || packageName == null) {
Context context = ReVancedUtils.getContext();
Context context = Utils.getContext();
resources = context.getResources();
packageName = context.getPackageName();
}
@ -110,11 +110,11 @@ public class StringRef {
if (resources != null) {
final int identifier = resources.getIdentifier(value, "string", packageName);
if (identifier == 0)
LogHelper.printException(() -> "Resource not found: " + value);
Logger.printException(() -> "Resource not found: " + value);
else
value = resources.getString(identifier);
} else {
LogHelper.printException(() -> "Could not resolve resources!");
Logger.printException(() -> "Could not resolve resources!");
}
}
return value;

View File

@ -1,7 +1,8 @@
package app.revanced.integrations.utils;
package app.revanced.integrations.shared;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@ -9,28 +10,43 @@ import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.*;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.settings.SettingsEnum;
import java.text.Bidi;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ReVancedUtils {
import app.revanced.integrations.shared.settings.BooleanSetting;
import kotlin.text.Regex;
public class Utils {
@SuppressLint("StaticFieldLeak")
public static Context context;
private static String versionName;
private ReVancedUtils() {
private Utils() {
} // utility class
public static String getVersionName() {
@ -51,7 +67,7 @@ public class ReVancedUtils {
0
);
} catch (PackageManager.NameNotFoundException e) {
LogHelper.printException(() -> "Failed to get package info", e);
Logger.printException(() -> "Failed to get package info", e);
return null;
}
@ -64,10 +80,10 @@ public class ReVancedUtils {
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy1dpUnderCondition(SettingsEnum condition, View view) {
if (!condition.getBoolean()) return;
public static void hideViewBy1dpUnderCondition(BooleanSetting condition, View view) {
if (!condition.get()) return;
LogHelper.printDebug(() -> "Hiding view with setting: " + condition);
Logger.printDebug(() -> "Hiding view with setting: " + condition);
hideViewByLayoutParams(view);
}
@ -78,10 +94,10 @@ public class ReVancedUtils {
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(SettingsEnum condition, View view) {
if (!condition.getBoolean()) return;
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
if (!condition.get()) return;
LogHelper.printDebug(() -> "Hiding view with setting: " + condition);
Logger.printDebug(() -> "Hiding view with setting: " + condition);
view.setVisibility(View.GONE);
}
@ -119,7 +135,7 @@ public class ReVancedUtils {
@SuppressWarnings("UnusedReturnValue")
public static long doNothingForDuration(long amountOfTimeToWaste) {
final long timeCalculationStarted = System.currentTimeMillis();
LogHelper.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
long meaninglessValue = 0;
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
@ -196,16 +212,26 @@ public class ReVancedUtils {
return null;
}
public static void restartApp(@NonNull Context context) {
String packageName = context.getPackageName();
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
// Required for API 34 and later
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
mainIntent.setPackage(packageName);
context.startActivity(mainIntent);
System.exit(0);
}
public interface MatchFilter<T> {
boolean matches(T object);
}
public static Context getContext() {
if (context != null) {
return context;
if (context == null) {
Logger.initializationError(Utils.class, "Context is null, returning null!", null);
}
LogHelper.printException(() -> "Context is null, returning null!");
return null;
return context;
}
public static void setClipboard(@NonNull String text) {
@ -249,11 +275,10 @@ public class ReVancedUtils {
private static void showToast(@NonNull String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
// cannot use getContext(), otherwise if context is null it will cause infinite recursion of error logging
if (context == null) {
LogHelper.printDebug(() -> "Cannot show toast (context is null)");
Logger.initializationError(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
} else {
LogHelper.printDebug(() -> "Showing toast: " + messageToToast);
Logger.printDebug(() -> "Showing toast: " + messageToToast);
Toast.makeText(context, messageToToast, toastDuration).show();
}
}
@ -277,7 +302,7 @@ public class ReVancedUtils {
try {
runnable.run();
} catch (Exception ex) {
LogHelper.printException(() -> runnable.getClass() + ": " + ex.getMessage(), ex);
Logger.printException(() -> runnable.getClass() + ": " + ex.getMessage(), ex);
}
};
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
@ -364,13 +389,52 @@ public class ReVancedUtils {
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(1, 1);
view.setLayoutParams(layoutParams5);
} else {
LogHelper.printDebug(() -> "Hidden view with id " + view.getId());
Logger.printDebug(() -> "Hidden view with id " + view.getId());
}
}
private static final Regex punctuationRegex = new Regex("\\p{P}+");
/**
* Sort the preferences by title and ignore the casing.
*
* Android Preferences are automatically sorted by title,
* but if using a localized string key it sorts on the key and not the actual title text that's used at runtime.
*
* @param menuDepthToSort Maximum menu depth to sort. Menus deeper than this value
* will show preferences in the order created in patches.
*/
public static void sortPreferenceGroupByTitle(PreferenceGroup group, int menuDepthToSort) {
if (menuDepthToSort == 0) return;
SortedMap<String, Preference> preferences = new TreeMap<>();
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference preference = group.getPreference(i);
if (preference instanceof PreferenceGroup) {
sortPreferenceGroupByTitle((PreferenceGroup) preference, menuDepthToSort - 1);
}
preferences.put(removePunctuationConvertToLowercase(preference.getTitle()), preference);
}
int prefIndex = 0;
for (Preference pref : preferences.values()) {
int indexToSet = prefIndex++;
if (pref instanceof PreferenceGroup || pref.getIntent() != null) {
// Place preference groups last.
// Use an offset to push the group to the end.
indexToSet += 1000;
}
pref.setOrder(indexToSet);
}
}
public static String removePunctuationConvertToLowercase(CharSequence original) {
return punctuationRegex.replace(original, "").toLowerCase();
}
public enum NetworkType {
NONE,
MOBILE,
OTHER,
}
}
}

View File

@ -0,0 +1,17 @@
package app.revanced.integrations.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.integrations.shared.settings.Setting.parent;
/**
* Settings shared across multiple apps.
*
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
* or reference this class.
*/
public class BaseSettings {
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
}

View File

@ -0,0 +1,79 @@
package app.revanced.integrations.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class BooleanSetting extends Setting<Boolean> {
public BooleanSetting(String key, Boolean defaultValue) {
super(key, defaultValue);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Boolean)} was intnded.
*/
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
setting.value = Objects.requireNonNull(newValue);
}
@Override
protected void load() {
value = preferences.getBoolean(key, defaultValue);
}
@Override
protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getBoolean(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Boolean.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Boolean newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveBoolean(key, newValue);
}
@NonNull
@Override
public Boolean get() {
return value;
}
}

View File

@ -0,0 +1,69 @@
package app.revanced.integrations.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class FloatSetting extends Setting<Float> {
public FloatSetting(String key, Float defaultValue) {
super(key, defaultValue);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public FloatSetting(String key, Float defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getFloatString(key, defaultValue);
}
@Override
protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return (float) json.getDouble(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Float.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Float newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveFloatString(key, newValue);
}
@NonNull
@Override
public Float get() {
return value;
}
}

View File

@ -0,0 +1,69 @@
package app.revanced.integrations.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class IntegerSetting extends Setting<Integer> {
public IntegerSetting(String key, Integer defaultValue) {
super(key, defaultValue);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getIntegerString(key, defaultValue);
}
@Override
protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getInt(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Integer.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Integer newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveIntegerString(key, newValue);
}
@NonNull
@Override
public Integer get() {
return value;
}
}

View File

@ -0,0 +1,69 @@
package app.revanced.integrations.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class LongSetting extends Setting<Long> {
public LongSetting(String key, Long defaultValue) {
super(key, defaultValue);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public LongSetting(String key, Long defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getLongString(key, defaultValue);
}
@Override
protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getLong(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Long.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Long newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveLongString(key, newValue);
}
@NonNull
@Override
public Long get() {
return value;
}
}

View File

@ -0,0 +1,429 @@
package app.revanced.integrations.shared.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.StringRef;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import static app.revanced.integrations.shared.StringRef.str;
@SuppressWarnings("unused")
public abstract class Setting<T> {
/**
* Indicates if a {@link Setting} is available to edit and use.
* Typically this is dependent upon other BooleanSetting(s) set to 'true',
* but this can be used to call into integrations code and check other conditions.
*/
public interface Availability {
boolean isAvailable();
}
/**
* Availability based on a single parent setting being enabled.
*/
@NonNull
public static Availability parent(@NonNull BooleanSetting parent) {
return parent::get;
}
/**
* Availability based on all parents being enabled.
*/
@NonNull
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (!parent.get()) return false;
}
return true;
};
}
/**
* Availability based on any parent being enabled.
*/
@NonNull
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (parent.get()) return true;
}
return false;
};
}
/**
* All settings that were instantiated.
* When a new setting is created, it is automatically added to this list.
*/
private static final List<Setting<?>> SETTINGS = new ArrayList<>();
/**
* Map of setting path to setting object.
*/
private static final Map<String, Setting<?>> PATH_TO_SETTINGS = new HashMap<>();
/**
* Preference all instances are saved to.
*/
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
@Nullable
public static Setting<?> getSettingFromPath(@NonNull String str) {
return PATH_TO_SETTINGS.get(str);
}
/**
* @return All settings that have been created.
*/
@NonNull
public static List<Setting<?>> allLoadedSettings() {
return Collections.unmodifiableList(SETTINGS);
}
/**
* @return All settings that have been created, sorted by keys.
*/
@NonNull
private static List<Setting<?>> allLoadedSettingsSorted() {
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
return Collections.unmodifiableList(SETTINGS);
}
/**
* The key used to store the value in the shared preferences.
*/
@NonNull
public final String key;
/**
* The default value of the setting.
*/
@NonNull
public final T defaultValue;
/**
* If the app should be rebooted, if this setting is changed
*/
public final boolean rebootApp;
/**
* If this setting should be included when importing/exporting settings.
*/
public final boolean includeWithImportExport;
/**
* If this setting is available to edit and use.
* Not to be confused with it's status returned from {@link #get()}.
*/
@Nullable
private final Availability availability;
/**
* Confirmation message to display, if the user tries to change the setting from the default value.
*/
@Nullable
public final StringRef userDialogMessage;
// Must be volatile, as some settings are read/write from different threads.
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
/**
* The value of the setting.
*/
@NonNull
protected volatile T value;
public Setting(String key, T defaultValue) {
this(key, defaultValue, false, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp) {
this(key, defaultValue, rebootApp, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
this(key, defaultValue, rebootApp, includeWithImportExport, null, null);
}
public Setting(String key, T defaultValue, String userDialogMessage) {
this(key, defaultValue, false, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, Availability availability) {
this(key, defaultValue, false, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
this(key, defaultValue, rebootApp, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) {
this(key, defaultValue, rebootApp, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
this(key, defaultValue, rebootApp, true, userDialogMessage, availability);
}
/**
* A setting backed by a shared preference.
*
* @param key The key used to store the value in the shared preferences.
* @param defaultValue The default value of the setting.
* @param rebootApp If the app should be rebooted, if this setting is changed.
* @param includeWithImportExport If this setting should be shown in the import/export dialog.
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
* @param availability Condition that must be true, for this setting to be available to configure.
*/
public Setting(@NonNull String key,
@NonNull T defaultValue,
boolean rebootApp,
boolean includeWithImportExport,
@Nullable String userDialogMessage,
@Nullable Availability availability
) {
this.key = Objects.requireNonNull(key);
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
this.rebootApp = rebootApp;
this.includeWithImportExport = includeWithImportExport;
this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage);
this.availability = availability;
SETTINGS.add(this);
if (PATH_TO_SETTINGS.put(key, this) != null) {
// Debug setting may not be created yet so using Logger may cause an initialization crash.
// Show a toast instead.
Utils.showToastLong(this.getClass().getSimpleName()
+ " error: Duplicate Setting key found: " + key);
}
load();
}
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
public static void migrateOldSettingToNew(@NonNull Setting<?> oldSetting, @NonNull Setting newSetting) {
if (!oldSetting.isSetToDefault()) {
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
//noinspection unchecked
newSetting.save(oldSetting.value);
oldSetting.resetToDefault();
}
}
/**
* Migrate an old Setting value previously stored in a different SharedPreference.
*
* This method will be deleted in the future.
*/
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
}
Object newValue = setting.get();
final Object migratedValue;
if (setting instanceof BooleanSetting) {
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
} else if (setting instanceof IntegerSetting) {
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
} else if (setting instanceof LongSetting) {
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
} else if (setting instanceof FloatSetting) {
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
} else if (setting instanceof StringSetting) {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
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);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Object)} was intended.
*/
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
setting.setValueFromString(newValue);
}
/**
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
*/
protected abstract void setValueFromString(@NonNull String newValue);
/**
* Load and set the value of {@link #value}.
*/
protected abstract void load();
/**
* Persistently saves the value.
*/
public abstract void save(@NonNull T newValue);
@NonNull
public abstract T get();
/**
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
*/
public void resetToDefault() {
save(defaultValue);
}
/**
* @return if this setting can be configured and used.
*/
public boolean isAvailable() {
return availability == null || availability.isAvailable();
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
}
@NotNull
@Override
public String toString() {
return key + "=" + get();
}
// region Import / export
/**
* If a setting path has this prefix, then remove it before importing/exporting.
*/
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
/**
* The path, minus any 'revanced' prefix to keep json concise.
*/
private String getImportExportKey() {
if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
}
return key;
}
/**
* @return the value stored using the import/export key. Do not set any values in this method.
*/
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
/**
* Saves this instance to JSON.
* <p>
* To keep the JSON simple and readable,
* subclasses should not write out any embedded types (such as JSON Array or Dictionaries).
* <p>
* If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long),
* then subclasses can override this method and write out a String value representing the value.
*/
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
json.put(importExportKey, value);
}
@NonNull
public static String exportToJson(@Nullable Context alertDialogContext) {
try {
JSONObject json = new JSONObject();
for (Setting<?> setting : allLoadedSettingsSorted()) {
String importExportKey = setting.getImportExportKey();
if (json.has(importExportKey)) {
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
}
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
//noinspection ConstantValue
if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) {
setting.writeToJSON(json, importExportKey);
}
}
SponsorBlockSettings.showExportWarningIfNeeded(alertDialogContext);
if (json.length() == 0) {
return "";
}
String export = json.toString(0);
// Remove the outer JSON braces to make the output more compact,
// and leave less chance of the user forgetting to copy it
return export.substring(2, export.length() - 2);
} catch (JSONException e) {
Logger.printException(() -> "Export failure", e); // should never happen
return "";
}
}
/**
* @return if any settings that require a reboot were changed.
*/
public static boolean importFromJSON(@NonNull String settingsJsonString) {
try {
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
}
JSONObject json = new JSONObject(settingsJsonString);
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
for (Setting setting : SETTINGS) {
String key = setting.getImportExportKey();
if (json.has(key)) {
Object value = setting.readFromJSON(json, key);
if (!setting.get().equals(value)) {
rebootSettingChanged |= setting.rebootApp;
//noinspection unchecked
setting.save(value);
}
numberOfSettingsImported++;
} else if (setting.includeWithImportExport && !setting.isSetToDefault()) {
Logger.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp;
setting.resetToDefault();
}
}
// SB Enum categories are saved using StringSettings.
// Which means they need to reload again if changed by other code (such as here).
// This call could be removed by creating a custom Setting class that manages the
// "String <-> Enum" logic or by adding an event hook of when settings are imported.
// But for now this is simple and works.
SponsorBlockSettings.updateFromImportedSettings();
Utils.showToastLong(numberOfSettingsImported == 0
? str("revanced_settings_import_reset")
: str("revanced_settings_import_success", numberOfSettingsImported));
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
Logger.printInfo(() -> "", ex);
} catch (Exception ex) {
Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
}
return false;
}
// End import / export
}

View File

@ -0,0 +1,69 @@
package app.revanced.integrations.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class StringSetting extends Setting<String> {
public StringSetting(String key, String defaultValue) {
super(key, defaultValue);
}
public StringSetting(String key, String defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public StringSetting(String key, String defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public StringSetting(String key, String defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getString(key, defaultValue);
}
@Override
protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getString(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Objects.requireNonNull(newValue);
}
@Override
public void save(@NonNull String newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveString(key, newValue);
}
@NonNull
@Override
public String get() {
return value;
}
}

View File

@ -0,0 +1,240 @@
package app.revanced.integrations.shared.settings.preference;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.*;
import androidx.annotation.NonNull;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.Setting;
import static app.revanced.integrations.shared.StringRef.str;
/**
*
*
* @noinspection deprecation, DataFlowIssue , unused */
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
*/
public static boolean settingImportInProgress;
/**
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
*/
private boolean showingUserDialogMessage;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) {
return;
}
Preference pref = findPreference(str);
if (pref == null) {
return;
}
Logger.printDebug(() -> "Preference changed: " + setting.key);
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
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(getContext());
}
}
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
};
/**
* Initialize this instance, and do any custom behavior.
* <p>
* To ensure all {@link Setting} instances are correctly synced to the UI,
* it is important that subclasses make a call or otherwise reference their Settings class bundle
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
final var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
Utils.sortPreferenceGroupByTitle(getPreferenceScreen(), 2);
}
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
final var context = getContext();
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(str("revanced_settings_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
})
.setOnDismissListener(dialog -> {
showingUserDialogMessage = false;
})
.setCancelable(false)
.show();
}
/**
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true);
}
/**
* Updates Preferences availability only using the status of {@link Setting}.
*/
protected void updateUIAvailability() {
updatePreferenceScreen(getPreferenceScreen(), false, false);
}
/**
* Syncs all UI Preferences to any {@link Setting} they represent.
*/
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
boolean syncSettingValue,
boolean applySettingToPreference) {
// 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
// the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i);
if (pref instanceof PreferenceScreen) {
updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
}
}
}
}
/**
* Updates a UI Preference with the {@link Setting} that backs it.
* If needed, subclasses can override this to handle additional UI Preference types.
*
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
protected void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
boolean syncSetting, boolean applySettingToPreference) {
if (!syncSetting && applySettingToPreference) {
throw new IllegalArgumentException();
}
if (syncSetting) {
if (pref instanceof SwitchPreference) {
SwitchPreference switchPref = (SwitchPreference) pref;
BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) {
switchPref.setChecked(boolSetting.get());
} else {
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference) {
EditTextPreference editPreference = (EditTextPreference) pref;
if (applySettingToPreference) {
editPreference.setText(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
if (applySettingToPreference) {
listPref.setValue(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, listPref.getValue());
}
updateListPreferenceSummary(listPref, setting);
} else {
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
return;
}
}
updatePreferenceAvailability(pref, setting);
}
protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting<?> setting) {
pref.setEnabled(setting.isAvailable());
}
protected void updateListPreferenceSummary(ListPreference listPreference, Setting<?> setting) {
String objectStringValue = setting.get().toString();
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
if (entryIndex >= 0) {
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
} else {
// Value is not an available option.
// User manually edited import data, or options changed and current selection is no longer available.
// Still show the value in the summary, so it's clear that something is selected.
listPreference.setSummary(objectStringValue);
}
}
public static void showRestartDialog(@NonNull final Context context) {
String positiveButton = str("revanced_settings_restart");
new AlertDialog.Builder(context).setMessage(str("revanced_settings_restart_title"))
.setPositiveButton(positiveButton, (dialog, id) -> {
Utils.restartApp(context);
})
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.show();
}
@SuppressLint("ResourceType")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(Setting.preferences.name);
// Must initialize before adding change listener,
// otherwise the syncing of Setting -> UI
// causes a callback to the listener even though nothing changed.
initialize();
updateUIToSettingValues();
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
} catch (Exception ex) {
Logger.printException(() -> "onCreate() failure", ex);
}
}
@Override
public void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
}

View File

@ -1,6 +1,4 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
package app.revanced.integrations.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
@ -11,11 +9,13 @@ import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
import static app.revanced.integrations.shared.StringRef.str;
/** @noinspection deprecation, unused */
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
@ -55,10 +55,10 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = SettingsEnum.exportJSON(getContext());
existingSettings = Setting.exportToJson(getContext());
getEditText().setText(existingSettings);
} catch (Exception ex) {
LogHelper.printException(() -> "showDialog failure", ex);
Logger.printException(() -> "showDialog failure", ex);
}
return true;
}
@ -68,12 +68,12 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
try {
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
ReVancedUtils.setClipboard(getEditText().getText().toString());
Utils.setClipboard(getEditText().getText().toString());
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
importSettings(getEditText().getText().toString());
});
} catch (Exception ex) {
LogHelper.printException(() -> "onPrepareDialogBuilder failure", ex);
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
@ -82,15 +82,15 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
if (replacementSettings.equals(existingSettings)) {
return;
}
ReVancedSettingsFragment.settingImportInProgress = true;
final boolean rebootNeeded = SettingsEnum.importJSON(replacementSettings);
AbstractPreferenceFragment.settingImportInProgress = true;
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings);
if (rebootNeeded) {
ReVancedSettingsFragment.showRestartDialog(getContext());
AbstractPreferenceFragment.showRestartDialog(getContext());
}
} catch (Exception ex) {
LogHelper.printException(() -> "importSettings failure", ex);
Logger.printException(() -> "importSettings failure", ex);
} finally {
ReVancedSettingsFragment.settingImportInProgress = false;
AbstractPreferenceFragment.settingImportInProgress = false;
}
}

View File

@ -1,6 +1,4 @@
package app.revanced.integrations.settingsmenu;
import static app.revanced.integrations.utils.StringRef.str;
package app.revanced.integrations.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
@ -9,12 +7,14 @@ import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.Logger;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import static app.revanced.integrations.shared.StringRef.str;
@SuppressWarnings("unused")
public class ResettableEditTextPreference extends EditTextPreference {
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -33,7 +33,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
SettingsEnum setting = SettingsEnum.settingFromPath(getKey());
Setting setting = Setting.getSettingFromPath(getKey());
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
@ -50,13 +50,13 @@ public class ResettableEditTextPreference extends EditTextPreference {
}
button.setOnClickListener(v -> {
try {
SettingsEnum setting = Objects.requireNonNull(SettingsEnum.settingFromPath(getKey()));
Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
String defaultStringValue = setting.defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
} catch (Exception ex) {
LogHelper.printException(() -> "reset failure", ex);
Logger.printException(() -> "reset failure", ex);
}
});
}

View File

@ -1,43 +1,37 @@
package app.revanced.integrations.settings;
package app.revanced.integrations.shared.settings.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceFragment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import java.util.Objects;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;
/**
* Shared categories, and helper methods.
*
* The various save methods store numbers as Strings,
* which is required if using {@link android.preference.PreferenceFragment}.
* which is required if using {@link PreferenceFragment}.
*
* If saved numbers will not be used with a preference fragment,
* then store the primitive numbers using {@link #preferences}.
* then store the primitive numbers using the {@link #preferences} itself.
*/
public enum SharedPrefCategory {
YOUTUBE("youtube"),
RETURN_YOUTUBE_DISLIKE("ryd"),
SPONSOR_BLOCK("sponsor-block"),
REVANCED_PREFS("revanced_prefs");
public class SharedPrefCategory {
@NonNull
public final String prefName;
public final String name;
@NonNull
public final SharedPreferences preferences;
SharedPrefCategory(@NonNull String prefName) {
this.prefName = Objects.requireNonNull(prefName);
preferences = Objects.requireNonNull(ReVancedUtils.getContext()).getSharedPreferences(prefName, Context.MODE_PRIVATE);
public SharedPrefCategory(@NonNull String name) {
this.name = Objects.requireNonNull(name);
preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE);
}
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
LogHelper.printException(() -> "Found conflicting preference: " + key);
Logger.printException(() -> "Found conflicting preference: " + key);
preferences.edit().remove(key).apply();
}
@ -89,7 +83,6 @@ public enum SharedPrefCategory {
}
}
public boolean getBoolean(@NonNull String key, boolean _default) {
try {
return preferences.getBoolean(key, _default);
@ -159,6 +152,6 @@ public enum SharedPrefCategory {
@NonNull
@Override
public String toString() {
return prefName;
return name;
}
}

View File

@ -1,396 +0,0 @@
package app.revanced.integrations.sponsorblock.objects;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.IGNORE;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import static app.revanced.integrations.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
import static app.revanced.integrations.utils.StringRef.sf;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.settings.SharedPrefCategory;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.StringRef;
public enum SegmentCategory {
SPONSOR("sponsor", sf("sb_segments_sponsor"), sf("sb_segments_sponsor_sum"), sf("sb_skip_button_sponsor"), sf("sb_skipped_sponsor"),
SKIP_AUTOMATICALLY_ONCE, 0x00D400),
SELF_PROMO("selfpromo", sf("sb_segments_selfpromo"), sf("sb_segments_selfpromo_sum"), sf("sb_skip_button_selfpromo"), sf("sb_skipped_selfpromo"),
MANUAL_SKIP, 0xFFFF00),
INTERACTION("interaction", sf("sb_segments_interaction"), sf("sb_segments_interaction_sum"), sf("sb_skip_button_interaction"), sf("sb_skipped_interaction"),
MANUAL_SKIP, 0xCC00FF),
/**
* Unique category that is treated differently than the rest.
*/
HIGHLIGHT("poi_highlight", sf("sb_segments_highlight"), sf("sb_segments_highlight_sum"), sf("sb_skip_button_highlight"), sf("sb_skipped_highlight"),
MANUAL_SKIP, 0xFF1684),
INTRO("intro", sf("sb_segments_intro"), sf("sb_segments_intro_sum"),
sf("sb_skip_button_intro_beginning"), sf("sb_skip_button_intro_middle"), sf("sb_skip_button_intro_end"),
sf("sb_skipped_intro_beginning"), sf("sb_skipped_intro_middle"), sf("sb_skipped_intro_end"),
MANUAL_SKIP, 0x00FFFF),
OUTRO("outro", sf("sb_segments_outro"), sf("sb_segments_outro_sum"), sf("sb_skip_button_outro"), sf("sb_skipped_outro"),
MANUAL_SKIP, 0x0202ED),
PREVIEW("preview", sf("sb_segments_preview"), sf("sb_segments_preview_sum"),
sf("sb_skip_button_preview_beginning"), sf("sb_skip_button_preview_middle"), sf("sb_skip_button_preview_end"),
sf("sb_skipped_preview_beginning"), sf("sb_skipped_preview_middle"), sf("sb_skipped_preview_end"),
IGNORE, 0x008FD6),
FILLER("filler", sf("sb_segments_filler"), sf("sb_segments_filler_sum"), sf("sb_skip_button_filler"), sf("sb_skipped_filler"),
IGNORE, 0x7300FF),
MUSIC_OFFTOPIC("music_offtopic", sf("sb_segments_nomusic"), sf("sb_segments_nomusic_sum"), sf("sb_skip_button_nomusic"), sf("sb_skipped_nomusic"),
MANUAL_SKIP, 0xFF9900),
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("sb_skip_button_unsubmitted"), sf("sb_skipped_unsubmitted"),
SKIP_AUTOMATICALLY, 0xFFFFFF);
private static final StringRef skipSponsorTextCompact = sf("sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("sb_skip_button_compact_highlight");
/**
* Prefix to use when serializing to flat JSON layout used with ReVanced import/export.
*/
private static final String FLAT_JSON_IMPORT_EXPORT_PREFIX = "sb_";
private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
INTRO,
OUTRO,
PREVIEW,
FILLER,
MUSIC_OFFTOPIC,
};
private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
HIGHLIGHT,
INTRO,
OUTRO,
PREVIEW,
FILLER,
MUSIC_OFFTOPIC,
};
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
private static final String COLOR_PREFERENCE_KEY_SUFFIX = "_color";
/**
* Categories currently enabled, formatted for an API call
*/
public static String sponsorBlockAPIFetchCategories = "[]";
static {
for (SegmentCategory value : categoriesWithoutUnsubmitted)
mValuesMap.put(value.key, value);
}
@NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
@NonNull
public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights;
}
@Nullable
public static SegmentCategory byCategoryKey(@NonNull String key) {
return mValuesMap.get(key);
}
public static void loadFromPreferences() {
SharedPreferences preferences = SharedPrefCategory.SPONSOR_BLOCK.preferences;
LogHelper.printDebug(() -> "loadFromPreferences");
for (SegmentCategory category : categoriesWithoutUnsubmitted()) {
category.load(preferences);
}
updateEnabledCategories();
}
/**
* Must be called if behavior of any category is changed
*/
public static void updateEnabledCategories() {
SegmentCategory[] categories = categoriesWithoutUnsubmitted();
List<String> enabledCategories = new ArrayList<>(categories.length);
for (SegmentCategory category : categories) {
if (category.behaviour != CategoryBehaviour.IGNORE) {
enabledCategories.add(category.key);
}
}
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
if (enabledCategories.isEmpty())
sponsorBlockAPIFetchCategories = "[]";
else
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
}
@NonNull
public final String key;
@NonNull
public final StringRef title;
@NonNull
public final StringRef description;
/**
* Skip button text, if the skip occurs in the first quarter of the video
*/
@NonNull
public final StringRef skipButtonTextBeginning;
/**
* Skip button text, if the skip occurs in the middle half of the video
*/
@NonNull
public final StringRef skipButtonTextMiddle;
/**
* Skip button text, if the skip occurs in the last quarter of the video
*/
@NonNull
public final StringRef skipButtonTextEnd;
/**
* Skipped segment toast, if the skip occurred in the first quarter of the video
*/
@NonNull
public final StringRef skippedToastBeginning;
/**
* Skipped segment toast, if the skip occurred in the middle half of the video
*/
@NonNull
public final StringRef skippedToastMiddle;
/**
* Skipped segment toast, if the skip occurred in the last quarter of the video
*/
@NonNull
public final StringRef skippedToastEnd;
@NonNull
public final Paint paint;
public final int defaultColor;
/**
* If value is changed, then also call {@link #save(SharedPreferences.Editor)}
*/
public int color;
/**
* If value is changed, then also call {@link #updateEnabledCategories()}
*/
@NonNull
public CategoryBehaviour behaviour;
@NonNull
public final CategoryBehaviour defaultBehaviour;
SegmentCategory(String key, StringRef title, StringRef description,
StringRef skipButtonText,
StringRef skippedToastText,
CategoryBehaviour defaultBehavior, int defaultColor) {
this(key, title, description,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
defaultBehavior, defaultColor);
}
SegmentCategory(String key, StringRef title, StringRef description,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
CategoryBehaviour defaultBehavior, int defaultColor) {
this.key = Objects.requireNonNull(key);
this.title = Objects.requireNonNull(title);
this.description = Objects.requireNonNull(description);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle);
this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd);
this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning);
this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle);
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviour = this.defaultBehaviour = Objects.requireNonNull(defaultBehavior);
this.color = this.defaultColor = defaultColor;
this.paint = new Paint();
setColor(defaultColor);
}
/**
* Caller must also call {@link #updateEnabledCategories()}
*/
private void load(SharedPreferences preferences) {
String categoryColor = preferences.getString(key + COLOR_PREFERENCE_KEY_SUFFIX, null);
if (categoryColor == null) {
setColor(defaultColor);
} else {
setColor(categoryColor);
}
String behaviorString = preferences.getString(key, null);
if (behaviorString == null) {
behaviour = defaultBehaviour;
} else {
CategoryBehaviour preferenceBehavior = CategoryBehaviour.byStringKey(behaviorString);
if (preferenceBehavior == null) {
LogHelper.printException(() -> "Unknown behavior: " + behaviorString); // should never happen
behaviour = defaultBehaviour;
} else {
behaviour = preferenceBehavior;
}
}
}
/**
* Saves the current color and behavior.
* Calling code is responsible for calling {@link SharedPreferences.Editor#apply()}
*/
public void save(SharedPreferences.Editor editor) {
String colorString = (color == defaultColor)
? null // remove any saved preference, so default is used on the next load
: colorString();
editor.putString(key + COLOR_PREFERENCE_KEY_SUFFIX, colorString);
editor.putString(key, behaviour.key);
}
private String getFlatJsonBehaviorKey() {
return FLAT_JSON_IMPORT_EXPORT_PREFIX + key;
}
private String getFlatJsonColorKey() {
return FLAT_JSON_IMPORT_EXPORT_PREFIX + key + COLOR_PREFERENCE_KEY_SUFFIX;
}
public void exportToFlatJSON(JSONObject json) throws JSONException {
if (behaviour != defaultBehaviour) {
json.put(getFlatJsonBehaviorKey(), behaviour.key);
}
if (color != defaultColor) {
json.put(getFlatJsonColorKey(), colorString());
}
}
/**
* Calling code is responsible for calling {@link #updateEnabledCategories()} and {@link SharedPreferences.Editor#apply()}
*/
public int importFromFlatJSON(JSONObject json, SharedPreferences.Editor editor) throws JSONException {
int numberOfSettingsImported = 0;
String behaviorKey = getFlatJsonBehaviorKey();
if (json.has(behaviorKey)) {
String behaviorString = json.getString(behaviorKey);
CategoryBehaviour importedBehavior = CategoryBehaviour.byStringKey(behaviorString);
if (importedBehavior == null) {
throw new IllegalArgumentException("unknown behavior: " + behaviorString);
}
behaviour = importedBehavior;
numberOfSettingsImported++;
} else {
behaviour = defaultBehaviour;
}
String colorKey = getFlatJsonColorKey();
if (json.has(colorKey)) {
setColor(json.getString(colorKey));
numberOfSettingsImported++;
} else {
color = defaultColor;
}
save(editor);
return numberOfSettingsImported;
}
/**
* @return HTML color format string
*/
@NonNull
public String colorString() {
return String.format("#%06X", color);
}
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
setColor(Color.parseColor(colorString));
}
public void setColor(int color) {
color &= 0xFFFFFF;
this.color = color;
paint.setColor(color);
paint.setAlpha(255);
}
@NonNull
private static String getCategoryColorDotHTML(int color) {
color &= 0xFFFFFF;
return String.format("<font color=\"#%06X\">⬀</font>", color);
}
@NonNull
public static Spanned getCategoryColorDot(int color) {
return Html.fromHtml(getCategoryColorDotHTML(color));
}
@NonNull
public Spanned getCategoryColorDot() {
return getCategoryColorDot(color);
}
@NonNull
public Spanned getTitleWithColorDot() {
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
}
/**
* @param segmentStartTime video time the segment category started
* @param videoLength length of the video
* @return the skip button text
*/
@NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (SettingsEnum.SB_COMPACT_SKIP_BUTTON.getBoolean()) {
return (this == SegmentCategory.HIGHLIGHT)
? skipSponsorTextCompactHighlight
: skipSponsorTextCompact;
}
if (videoLength == 0) {
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skipButtonTextBeginning;
} else if (position < 0.75f) {
return skipButtonTextMiddle;
}
return skipButtonTextEnd;
}
/**
* @param segmentStartTime video time the segment category started
* @param videoLength length of the video
* @return 'skipped segment' toast message
*/
@NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skippedToastBeginning; // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skippedToastBeginning;
} else if (position < 0.75f) {
return skippedToastMiddle;
}
return skippedToastEnd;
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.integrations.tiktok;
import app.revanced.integrations.shared.settings.StringSetting;
public class Utils {
// Edit: This could be handled using a custom Setting<Long[]> class
// that saves its value to preferences and JSON using the formatted String created here.
public static long[] parseMinMax(StringSetting setting) {
final String[] minMax = setting.get().split("-");
if (minMax.length == 2) {
try {
final long min = Long.parseLong(minMax[0]);
final long max = Long.parseLong(minMax[1]);
if (min <= max && min >= 0) return new long[]{min, max};
} catch (NumberFormatException ignored) {
}
}
setting.save("0-" + Long.MAX_VALUE);
return new long[]{0L, Long.MAX_VALUE};
}
}

View File

@ -0,0 +1,13 @@
package app.revanced.integrations.tiktok.cleardisplay;
import app.revanced.integrations.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class RememberClearDisplayPatch {
public static boolean getClearDisplayState() {
return Settings.CLEAR_DISPLAY.get();
}
public static void rememberClearDisplayState(boolean newState) {
Settings.CLEAR_DISPLAY.save(newState);
}
}

View File

@ -0,0 +1,14 @@
package app.revanced.integrations.tiktok.download;
import app.revanced.integrations.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class DownloadsPatch {
public static String getDownloadPath() {
return Settings.DOWNLOAD_PATH.get();
}
public static boolean shouldRemoveWatermark() {
return Settings.DOWNLOAD_WATERMARK.get();
}
}

View File

@ -1,12 +1,12 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class AdsFilter implements IFilter {
@Override
public boolean getEnabled() {
return SettingsEnum.REMOVE_ADS.getBoolean();
return Settings.REMOVE_ADS.get();
}
@Override

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.FeedItemList;

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;

View File

@ -1,12 +1,12 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class ImageVideoFilter implements IFilter {
@Override
public boolean getEnabled() {
return SettingsEnum.HIDE_IMAGE.getBoolean();
return Settings.HIDE_IMAGE.get();
}
@Override

View File

@ -1,17 +1,17 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.tiktok.utils.ReVancedUtils.parseMinMax;
import static app.revanced.integrations.tiktok.Utils.parseMinMax;
public final class LikeCountFilter implements IFilter {
final long minLike;
final long maxLike;
LikeCountFilter() {
long[] minMax = parseMinMax(SettingsEnum.MIN_MAX_LIKES);
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
minLike = minMax[0];
maxLike = minMax[1];
}

View File

@ -1,12 +1,12 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class LiveFilter implements IFilter {
@Override
public boolean getEnabled() {
return SettingsEnum.HIDE_LIVE.getBoolean();
return Settings.HIDE_LIVE.get();
}
@Override

View File

@ -1,12 +1,12 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class StoryFilter implements IFilter {
@Override
public boolean getEnabled() {
return SettingsEnum.HIDE_STORY.getBoolean();
return Settings.HIDE_STORY.get();
}
@Override

View File

@ -1,17 +1,17 @@
package app.revanced.tiktok.feedfilter;
package app.revanced.integrations.tiktok.feedfilter;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.tiktok.utils.ReVancedUtils.parseMinMax;
import static app.revanced.integrations.tiktok.Utils.parseMinMax;
public class ViewCountFilter implements IFilter {
final long minView;
final long maxView;
ViewCountFilter() {
long[] minMax = parseMinMax(SettingsEnum.MIN_MAX_VIEWS);
long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS);
minView = minMax[0];
maxView = minMax[1];
}

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.settingsmenu;
package app.revanced.integrations.tiktok.settings;
import android.content.Context;
import android.content.Intent;
@ -7,17 +7,22 @@ import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.tiktok.settings.preference.ReVancedPreferenceFragment;
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import app.revanced.tiktok.utils.LogHelper;
import app.revanced.tiktok.utils.ReVancedUtils;
public class SettingsMenu {
/**
* Hooks AdPersonalizationActivity.
* <p>
* This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
*
* @noinspection unused
*/
public class AdPersonalizationActivityHook {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
try {
Class<?> entryClazz = Class.forName(entryClazzName);
@ -26,7 +31,8 @@ public class SettingsMenu {
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
return entryConstructor.newInstance(buttonInfo);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
throw new RuntimeException(e);
}
}
@ -36,7 +42,7 @@ public class SettingsMenu {
* @param base The activity to initialize the settings menu on.
* @return Whether the settings menu should be initialized.
*/
public static boolean initializeSettings(AdPersonalizationActivity base) {
public static boolean initialize(AdPersonalizationActivity base) {
Bundle extras = base.getIntent().getExtras();
if (extras != null && !extras.getBoolean("revanced", false)) return false;
@ -63,14 +69,14 @@ public class SettingsMenu {
}
private static void startSettingsActivity() {
Context appContext = ReVancedUtils.getAppContext();
Context appContext = Utils.getContext();
if (appContext != null) {
Intent intent = new Intent(appContext, AdPersonalizationActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("revanced", true);
appContext.startActivity(intent);
} else {
LogHelper.debug(SettingsMenu.class, "ReVancedUtils.getAppContext() return null");
Logger.printDebug(() -> "Utils.getContext() return null");
}
}
}

View File

@ -0,0 +1,26 @@
package app.revanced.integrations.tiktok.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.FloatSetting;
import app.revanced.integrations.shared.settings.StringSetting;
public class Settings extends BaseSettings {
public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true);
public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true);
public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true);
public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true);
public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true);
public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true);
public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok");
public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE);
public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE);
public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f);
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true);
public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us");
public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160");
public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile");
}

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.settingsmenu;
package app.revanced.integrations.tiktok.settings;
public class SettingsStatus {
public static boolean feedFilterEnabled = false;

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.settingsmenu.preference;
package app.revanced.integrations.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
@ -15,7 +15,7 @@ import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class DownloadPathPreference extends DialogPreference {
@ -27,13 +27,13 @@ public class DownloadPathPreference extends DialogPreference {
private int mediaPathIndex;
private String childDownloadPath;
public DownloadPathPreference(Context context, String title, SettingsEnum setting) {
public DownloadPathPreference(Context context, String title, StringSetting setting) {
super(context);
this.context = context;
this.setTitle(title);
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.getString());
this.setKey(setting.path);
this.setValue(setting.getString());
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
this.setKey(setting.key);
this.setValue(setting.get());
}
public String getValue() {

View File

@ -1,17 +1,17 @@
package app.revanced.tiktok.settingsmenu.preference;
package app.revanced.integrations.tiktok.settings.preference;
import android.content.Context;
import android.preference.EditTextPreference;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.shared.settings.StringSetting;
public class InputTextPreference extends EditTextPreference {
public InputTextPreference(Context context, String title, String summary, SettingsEnum setting) {
public InputTextPreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.path);
this.setText(setting.getString());
this.setKey(setting.key);
this.setText(setting.get());
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.settingsmenu.preference;
package app.revanced.integrations.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
@ -13,7 +13,7 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class RangeValuePreference extends DialogPreference {
@ -27,13 +27,13 @@ public class RangeValuePreference extends DialogPreference {
private boolean mValueSet;
public RangeValuePreference(Context context, String title, String summary, SettingsEnum setting) {
public RangeValuePreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.context = context;
setTitle(title);
setSummary(summary);
setKey(setting.path);
setValue(setting.getString());
setKey(setting.key);
setValue(setting.get());
}
public void setValue(String value) {

View File

@ -0,0 +1,29 @@
package app.revanced.integrations.tiktok.settings.preference;
import android.preference.PreferenceScreen;
import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.integrations.tiktok.settings.preference.categories.DownloadsPreferenceCategory;
import app.revanced.integrations.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
import app.revanced.integrations.tiktok.settings.preference.categories.IntegrationsPreferenceCategory;
import app.revanced.integrations.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
/**
* Preference fragment for ReVanced settings
*/
@SuppressWarnings("deprecation")
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void initialize() {
final var context = getContext();
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
// Custom categories reference app specific Settings class.
new FeedFilterPreferenceCategory(context, preferenceScreen);
new DownloadsPreferenceCategory(context, preferenceScreen);
new SimSpoofPreferenceCategory(context, preferenceScreen);
new IntegrationsPreferenceCategory(context, preferenceScreen);
}
}

View File

@ -1,17 +1,17 @@
package app.revanced.tiktok.settingsmenu.preference;
package app.revanced.integrations.tiktok.settings.preference;
import android.content.Context;
import android.preference.SwitchPreference;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.shared.settings.BooleanSetting;
@SuppressWarnings("deprecation")
public class TogglePreference extends SwitchPreference {
public TogglePreference(Context context, String title, String summary, SettingsEnum setting) {
public TogglePreference(Context context, String title, String summary, BooleanSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.path);
this.setChecked(setting.getBoolean());
this.setKey(setting.key);
this.setChecked(setting.get());
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.tiktok.settingsmenu.preference.categories;
package app.revanced.integrations.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceCategory;

View File

@ -1,11 +1,11 @@
package app.revanced.tiktok.settingsmenu.preference.categories;
package app.revanced.integrations.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.tiktok.settingsmenu.SettingsStatus;
import app.revanced.tiktok.settingsmenu.preference.DownloadPathPreference;
import app.revanced.tiktok.settingsmenu.preference.TogglePreference;
import app.revanced.integrations.tiktok.settings.Settings;
import app.revanced.integrations.tiktok.settings.SettingsStatus;
import app.revanced.integrations.tiktok.settings.preference.DownloadPathPreference;
import app.revanced.integrations.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory {
@ -24,12 +24,12 @@ public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory {
addPreference(new DownloadPathPreference(
context,
"Download path",
SettingsEnum.DOWNLOAD_PATH
Settings.DOWNLOAD_PATH
));
addPreference(new TogglePreference(
context,
"Remove watermark", "",
SettingsEnum.DOWNLOAD_WATERMARK
Settings.DOWNLOAD_WATERMARK
));
}
}

View File

@ -1,11 +1,11 @@
package app.revanced.tiktok.settingsmenu.preference.categories;
package app.revanced.integrations.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.tiktok.settingsmenu.SettingsStatus;
import app.revanced.tiktok.settingsmenu.preference.RangeValuePreference;
import app.revanced.tiktok.settingsmenu.preference.TogglePreference;
import app.revanced.integrations.tiktok.settings.preference.RangeValuePreference;
import app.revanced.integrations.tiktok.settings.Settings;
import app.revanced.integrations.tiktok.settings.SettingsStatus;
import app.revanced.integrations.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory {
@ -24,32 +24,32 @@ public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory
addPreference(new TogglePreference(
context,
"Remove feed ads", "Remove ads from feed.",
SettingsEnum.REMOVE_ADS
Settings.REMOVE_ADS
));
addPreference(new TogglePreference(
context,
"Hide livestreams", "Hide livestreams from feed.",
SettingsEnum.HIDE_LIVE
Settings.HIDE_LIVE
));
addPreference(new TogglePreference(
context,
"Hide story", "Hide story from feed.",
SettingsEnum.HIDE_STORY
Settings.HIDE_STORY
));
addPreference(new TogglePreference(
context,
"Hide image video", "Hide image video from feed.",
SettingsEnum.HIDE_IMAGE
Settings.HIDE_IMAGE
));
addPreference(new RangeValuePreference(
context,
"Min/Max views", "The minimum or maximum views of a video to show.",
SettingsEnum.MIN_MAX_VIEWS
Settings.MIN_MAX_VIEWS
));
addPreference(new RangeValuePreference(
context,
"Min/Max likes", "The minimum or maximum likes of a video to show.",
SettingsEnum.MIN_MAX_LIKES
Settings.MIN_MAX_LIKES
));
}
}

View File

@ -1,9 +1,10 @@
package app.revanced.tiktok.settingsmenu.preference.categories;
package app.revanced.integrations.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.tiktok.settingsmenu.preference.TogglePreference;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class IntegrationsPreferenceCategory extends ConditionalPreferenceCategory {
@ -22,7 +23,7 @@ public class IntegrationsPreferenceCategory extends ConditionalPreferenceCategor
addPreference(new TogglePreference(context,
"Enable debug log",
"Show integration debug log.",
SettingsEnum.DEBUG
BaseSettings.DEBUG
));
}
}

View File

@ -1,11 +1,11 @@
package app.revanced.tiktok.settingsmenu.preference.categories;
package app.revanced.integrations.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.tiktok.settingsmenu.SettingsStatus;
import app.revanced.tiktok.settingsmenu.preference.InputTextPreference;
import app.revanced.tiktok.settingsmenu.preference.TogglePreference;
import app.revanced.integrations.tiktok.settings.Settings;
import app.revanced.integrations.tiktok.settings.SettingsStatus;
import app.revanced.integrations.tiktok.settings.preference.InputTextPreference;
import app.revanced.integrations.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory {
@ -26,22 +26,22 @@ public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory {
context,
"Fake sim card info",
"Bypass regional restriction by fake sim card information.",
SettingsEnum.SIM_SPOOF
Settings.SIM_SPOOF
));
addPreference(new InputTextPreference(
context,
"Country ISO", "us, uk, jp, ...",
SettingsEnum.SIM_SPOOF_ISO
Settings.SIM_SPOOF_ISO
));
addPreference(new InputTextPreference(
context,
"Operator mcc+mnc", "mcc+mnc",
SettingsEnum.SIMSPOOF_MCCMNC
Settings.SIMSPOOF_MCCMNC
));
addPreference(new InputTextPreference(
context,
"Operator name", "Name of the operator.",
SettingsEnum.SIMSPOOF_OP_NAME
Settings.SIMSPOOF_OP_NAME
));
}
}

View File

@ -0,0 +1,13 @@
package app.revanced.integrations.tiktok.speed;
import app.revanced.integrations.tiktok.settings.Settings;
public class PlaybackSpeedPatch {
public static void rememberPlaybackSpeed(float newSpeed) {
Settings.REMEMBERED_SPEED.save(newSpeed);
}
public static float getPlaybackSpeed() {
return Settings.REMEMBERED_SPEED.get();
}
}

View File

@ -1,14 +1,15 @@
package app.revanced.tiktok.spoof.sim;
package app.revanced.integrations.tiktok.spoof.sim;
import app.revanced.tiktok.settings.SettingsEnum;
import app.revanced.integrations.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class SpoofSimPatch {
public static boolean isEnable() {
return SettingsEnum.SIM_SPOOF.getBoolean();
return Settings.SIM_SPOOF.get();
}
public static String getCountryIso(String value) {
if (isEnable()) {
return SettingsEnum.SIM_SPOOF_ISO.getString();
return Settings.SIM_SPOOF_ISO.get();
} else {
return value;
}
@ -16,14 +17,14 @@ public class SpoofSimPatch {
}
public static String getOperator(String value) {
if (isEnable()) {
return SettingsEnum.SIMSPOOF_MCCMNC.getString();
return Settings.SIMSPOOF_MCCMNC.get();
} else {
return value;
}
}
public static String getOperatorName(String value) {
if (isEnable()) {
return SettingsEnum.SIMSPOOF_OP_NAME.getString();
return Settings.SIMSPOOF_OP_NAME.get();
} else {
return value;
}

View File

@ -1,4 +1,4 @@
package app.revanced.tudortmund.lockscreen;
package app.revanced.integrations.tudortmund.lockscreen;
import android.content.Context;
import android.hardware.display.DisplayManager;

View File

@ -1,4 +1,4 @@
package app.revanced.tumblr.patches;
package app.revanced.integrations.tumblr.patches;
import com.tumblr.rumblr.model.TimelineObject;
import com.tumblr.rumblr.model.Timelineable;

View File

@ -0,0 +1,14 @@
package app.revanced.integrations.twitch;
public class Utils {
/* Called from SettingsPatch smali */
public static int getStringId(String name) {
return app.revanced.integrations.shared.Utils.getResourceIdentifier(name, "string");
}
/* Called from SettingsPatch smali */
public static int getDrawableId(String name) {
return app.revanced.integrations.shared.Utils.getResourceIdentifier(name, "drawable");
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.twitch.adblock;
package app.revanced.integrations.twitch.adblock;
import okhttp3.Request;

View File

@ -1,14 +1,15 @@
package app.revanced.twitch.adblock;
package app.revanced.integrations.twitch.adblock;
import app.revanced.twitch.utils.LogHelper;
import app.revanced.twitch.utils.ReVancedUtils;
import app.revanced.integrations.shared.Logger;
import okhttp3.HttpUrl;
import okhttp3.Request;
import static app.revanced.integrations.shared.StringRef.str;
public class LuminousService implements IAdblockService {
@Override
public String friendlyName() {
return ReVancedUtils.getString("revanced_proxy_luminous");
return str("revanced_proxy_luminous");
}
@Override
@ -33,7 +34,7 @@ public class LuminousService implements IAdblockService {
);
if (url == null) {
LogHelper.error("Failed to parse rewritten URL");
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}

View File

@ -1,14 +1,16 @@
package app.revanced.twitch.adblock;
package app.revanced.integrations.twitch.adblock;
import app.revanced.twitch.api.RetrofitClient;
import app.revanced.twitch.utils.LogHelper;
import app.revanced.twitch.utils.ReVancedUtils;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.twitch.api.RetrofitClient;
import okhttp3.HttpUrl;
import okhttp3.Request;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static app.revanced.integrations.shared.StringRef.str;
public class PurpleAdblockService implements IAdblockService {
private final Map<String, Boolean> tunnels = new HashMap<>() {{
put("https://eu1.jupter.ga", false);
@ -17,7 +19,7 @@ public class PurpleAdblockService implements IAdblockService {
@Override
public String friendlyName() {
return ReVancedUtils.getString("revanced_proxy_purpleadblock");
return str("revanced_proxy_purpleadblock");
}
@Override
@ -33,19 +35,27 @@ public class PurpleAdblockService implements IAdblockService {
try {
var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute();
if (!response.isSuccessful()) {
LogHelper.error("PurpleAdBlock tunnel $tunnel returned an error: HTTP code %d", response.code());
LogHelper.debug(response.message());
Logger.printException(() ->
"PurpleAdBlock tunnel $tunnel returned an error: HTTP code " + response.code()
);
Logger.printDebug(response::message);
try (var errorBody = response.errorBody()) {
if (errorBody != null) {
LogHelper.debug(errorBody.string());
Logger.printDebug(() -> {
try {
return errorBody.string();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
success = false;
}
} catch (Exception ex) {
LogHelper.printException("PurpleAdBlock tunnel $tunnel is unavailable", ex);
Logger.printException(() -> "PurpleAdBlock tunnel $tunnel is unavailable", ex);
success = false;
}
@ -69,7 +79,7 @@ public class PurpleAdblockService implements IAdblockService {
// Compose new URL
var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest));
if (url == null) {
LogHelper.error("Failed to parse rewritten URL");
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}
@ -80,7 +90,7 @@ public class PurpleAdblockService implements IAdblockService {
.build();
}
LogHelper.error("No tunnels are available");
Logger.printException(() -> "No tunnels are available");
return null;
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.twitch.api;
package app.revanced.integrations.twitch.api;
import okhttp3.ResponseBody;
import retrofit2.Call;

View File

@ -0,0 +1,120 @@
package app.revanced.integrations.twitch.api;
import androidx.annotation.NonNull;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.twitch.adblock.IAdblockService;
import app.revanced.integrations.twitch.adblock.LuminousService;
import app.revanced.integrations.twitch.adblock.PurpleAdblockService;
import app.revanced.integrations.twitch.settings.Settings;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
import static app.revanced.integrations.shared.StringRef.str;
public class RequestInterceptor implements Interceptor {
private IAdblockService activeService = null;
private static final String PROXY_DISABLED = str("key_revanced_proxy_disabled");
private static final String LUMINOUS_SERVICE = str("key_revanced_proxy_luminous");
private static final String PURPLE_ADBLOCK_SERVICE = str("key_revanced_proxy_purpleadblock");
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
var originalRequest = chain.request();
if (Settings.BLOCK_EMBEDDED_ADS.get().equals(PROXY_DISABLED)) {
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Intercepted request to URL:" + originalRequest.url());
// Skip if not HLS manifest request
if (!originalRequest.url().host().contains("usher.ttvnw.net")) {
return chain.proceed(originalRequest);
}
final String isVod;
if (IAdblockService.isVod(originalRequest)) isVod = "yes";
else isVod = "no";
Logger.printDebug(() -> "Found HLS manifest request. Is VOD? " +
isVod +
"; Channel: " +
IAdblockService.channelName(originalRequest)
);
// None of the services support VODs currently
if (IAdblockService.isVod(originalRequest)) return chain.proceed(originalRequest);
updateActiveService();
if (activeService != null) {
var available = activeService.isAvailable();
var rewritten = activeService.rewriteHlsRequest(originalRequest);
if (!available || rewritten == null) {
Utils.showToastShort(String.format(
str("revanced_embedded_ads_service_unavailable"), activeService.friendlyName()
));
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Rewritten HLS stream URL: " + rewritten.url());
var maxAttempts = activeService.maxAttempts();
for (var i = 1; i <= maxAttempts; i++) {
// Execute rewritten request and close body to allow multiple proceed() calls
var response = chain.proceed(rewritten);
response.close();
if (!response.isSuccessful()) {
int attempt = i;
Logger.printException(() -> "Request failed (attempt " +
attempt +
"/" + maxAttempts + "): HTTP error " +
response.code() +
" (" + response.message() + ")"
);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Logger.printException(() -> "Failed to sleep", e);
}
} else {
// Accept response from ad blocker
Logger.printDebug(() -> "Ad-blocker used");
return chain.proceed(rewritten);
}
}
// maxAttempts exceeded; giving up on using the ad blocker
Utils.showToastLong(String.format(
str("revanced_embedded_ads_service_failed"),
activeService.friendlyName())
);
}
// Adblock disabled
return chain.proceed(originalRequest);
}
private void updateActiveService() {
var current = Settings.BLOCK_EMBEDDED_ADS.get();
if (current.equals(LUMINOUS_SERVICE) && !(activeService instanceof LuminousService))
activeService = new LuminousService();
else if (current.equals(PURPLE_ADBLOCK_SERVICE) && !(activeService instanceof PurpleAdblockService))
activeService = new PurpleAdblockService();
else if (current.equals(PROXY_DISABLED))
activeService = null;
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.twitch.api;
package app.revanced.integrations.twitch.api;
import retrofit2.Retrofit;

View File

@ -0,0 +1,10 @@
package app.revanced.integrations.twitch.patches;
import app.revanced.integrations.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AudioAdsPatch {
public static boolean shouldBlockAudioAds() {
return Settings.BLOCK_AUDIO_ADS.get();
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.integrations.twitch.patches;
import app.revanced.integrations.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AutoClaimChannelPointsPatch {
public static boolean shouldAutoClaim() {
return Settings.AUTO_CLAIM_CHANNEL_POINTS.get();
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.integrations.twitch.patches;
import app.revanced.integrations.twitch.settings.Settings;
@SuppressWarnings("unused")
public class DebugModePatch {
public static boolean isDebugModeEnabled() {
return Settings.TWITCH_DEBUG_MODE.get();
}
}

View File

@ -1,7 +1,8 @@
package app.revanced.twitch.patches;
package app.revanced.integrations.twitch.patches;
import app.revanced.twitch.api.RequestInterceptor;
import app.revanced.integrations.twitch.api.RequestInterceptor;
@SuppressWarnings("unused")
public class EmbeddedAdsPatch {
public static RequestInterceptor createRequestInterceptor() {
return new RequestInterceptor();

View File

@ -1,4 +1,6 @@
package app.revanced.twitch.patches;
package app.revanced.integrations.twitch.patches;
import static app.revanced.integrations.shared.StringRef.str;
import android.graphics.Color;
import android.graphics.Typeface;
@ -9,28 +11,33 @@ import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import java.util.Objects;
import androidx.annotation.Nullable;
import app.revanced.twitch.settings.SettingsEnum;
import app.revanced.twitch.utils.ReVancedUtils;
import app.revanced.integrations.twitch.settings.Settings;
import tv.twitch.android.shared.chat.util.ClickableUsernameSpan;
@SuppressWarnings("unused")
public class ShowDeletedMessagesPatch {
/**
* Injection point.
*/
public static boolean shouldUseSpoiler() {
return Objects.equals(SettingsEnum.SHOW_DELETED_MESSAGES.getString(), "spoiler");
return "spoiler".equals(Settings.SHOW_DELETED_MESSAGES.get());
}
public static boolean shouldCrossOut() {
return Objects.equals(SettingsEnum.SHOW_DELETED_MESSAGES.getString(), "cross-out");
return "cross-out".equals(Settings.SHOW_DELETED_MESSAGES.get());
}
@Nullable
public static Spanned reformatDeletedMessage(Spanned original) {
if (!shouldCrossOut())
return null;
SpannableStringBuilder ssb = new SpannableStringBuilder(original);
ssb.setSpan(new StrikethroughSpan(), 0, original.length(), 0);
ssb.append(" (").append(ReVancedUtils.getString("revanced_deleted_msg")).append(")");
ssb.append(" (").append(str("revanced_deleted_msg")).append(")");
ssb.setSpan(new StyleSpan(Typeface.ITALIC), original.length(), ssb.length(), 0);
// Gray-out username

View File

@ -0,0 +1,10 @@
package app.revanced.integrations.twitch.patches;
import app.revanced.integrations.twitch.settings.Settings;
@SuppressWarnings("unused")
public class VideoAdsPatch {
public static boolean shouldBlockVideoAds() {
return Settings.BLOCK_VIDEO_ADS.get();
}
}

View File

@ -1,23 +1,24 @@
package app.revanced.twitch.settingsmenu;
import static app.revanced.twitch.utils.ReVancedUtils.getIdentifier;
import static app.revanced.twitch.utils.ReVancedUtils.getStringId;
package app.revanced.integrations.twitch.settings;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.twitch.settings.preference.ReVancedPreferenceFragment;
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
import tv.twitch.android.settings.SettingsActivity;
import java.util.ArrayList;
import java.util.List;
import app.revanced.twitch.utils.ReVancedUtils;
import app.revanced.twitch.utils.LogHelper;
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
import tv.twitch.android.settings.SettingsActivity;
public class SettingsHooks {
/**
* Hooks AppCompatActivity.
* <p>
* This class is responsible for injecting our own fragment by replacing the AppCompatActivity.
* @noinspection unused
*/
public class AppCompatActivityHook {
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
@ -25,16 +26,18 @@ public class SettingsHooks {
* Launches SettingsActivity and show ReVanced settings
*/
public static void startSettingsActivity() {
LogHelper.debug("Launching ReVanced settings");
Logger.printDebug(() -> "Launching ReVanced settings");
ReVancedUtils.ifContextAttached((c) -> {
Intent intent = new Intent(c, SettingsActivity.class);
final var context = app.revanced.integrations.shared.Utils.getContext();
if (context != null) {
Intent intent = new Intent(context, SettingsActivity.class);
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_REVANCED_SETTINGS, true);
intent.putExtras(bundle);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
c.startActivity(intent);
});
context.startActivity(intent);
}
}
/**
@ -42,7 +45,7 @@ public class SettingsHooks {
* @return Returns string resource id
*/
public static int getReVancedSettingsString() {
return getStringId("revanced_settings");
return app.revanced.integrations.twitch.Utils.getStringId("revanced_settings");
}
/**
@ -52,13 +55,12 @@ public class SettingsHooks {
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
if(groups.size() < 1) {
if (groups.isEmpty()) {
// Create new menu group if none exist yet
List<Object> items = new ArrayList<>();
items.add(revancedEntry);
groups.add(new SettingsMenuGroup(items));
}
else {
} else {
// Add to last menu group
int groupIdx = groups.size() - 1;
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
@ -66,7 +68,7 @@ public class SettingsHooks {
groups.add(new SettingsMenuGroup(items));
}
LogHelper.debug("%d menu groups in list", settingGroups.size());
Logger.printDebug(() -> settingGroups.size() + " menu groups in list");
return groups;
}
@ -76,8 +78,8 @@ public class SettingsHooks {
*/
@SuppressWarnings("rawtypes")
public static boolean handleSettingMenuOnClick(Enum item) {
LogHelper.debug("item %d clicked", item.ordinal());
if(item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) {
Logger.printDebug(() -> "item " + item.ordinal() + " clicked");
if (item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) {
return false;
}
@ -89,21 +91,21 @@ public class SettingsHooks {
* Intercepts fragment loading in SettingsActivity.onCreate
* @return Returns true if the revanced settings have been requested by the user, otherwise false
*/
public static boolean handleSettingsCreation(AppCompatActivity base) {
if(!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
LogHelper.debug("Revanced settings not requested");
public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) {
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
Logger.printDebug(() -> "Revanced settings not requested");
return false; // User wants to enter another settings fragment
}
LogHelper.debug("ReVanced settings requested");
Logger.printDebug(() -> "ReVanced settings requested");
ReVancedSettingsFragment fragment = new ReVancedSettingsFragment();
ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment();
ActionBar supportActionBar = base.getSupportActionBar();
if(supportActionBar != null)
supportActionBar.setTitle(getStringId("revanced_settings"));
if (supportActionBar != null)
supportActionBar.setTitle(app.revanced.integrations.twitch.Utils.getStringId("revanced_settings"));
base.getFragmentManager()
.beginTransaction()
.replace(getIdentifier("fragment_container", "id"), fragment)
.replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment)
.commit();
return true;
}

View File

@ -0,0 +1,25 @@
package app.revanced.integrations.twitch.settings;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.StringSetting;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
public class Settings extends BaseSettings {
/* Ads */
public static final BooleanSetting BLOCK_VIDEO_ADS = new BooleanSetting("revanced_block_video_ads", TRUE);
public static final BooleanSetting BLOCK_AUDIO_ADS = new BooleanSetting("revanced_block_audio_ads", TRUE);
public static final StringSetting BLOCK_EMBEDDED_ADS = new StringSetting("revanced_block_embedded_ads", "luminous");
/* Chat */
public static final StringSetting SHOW_DELETED_MESSAGES = new StringSetting("revanced_show_deleted_messages", "cross-out");
public static final BooleanSetting AUTO_CLAIM_CHANNEL_POINTS = new BooleanSetting("revanced_auto_claim_channel_points", TRUE);
/* Misc */
/**
* Not to be confused with {@link BaseSettings#DEBUG}.
*/
public static final BooleanSetting TWITCH_DEBUG_MODE = new BooleanSetting("revanced_twitch_debug_mode", FALSE, true);
}

View File

@ -1,4 +1,4 @@
package app.revanced.twitch.settingsmenu.preference;
package app.revanced.integrations.twitch.settings.preference;
import android.content.Context;
import android.graphics.Color;

View File

@ -0,0 +1,21 @@
package app.revanced.integrations.twitch.settings.preference;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.integrations.twitch.settings.Settings;
/**
* Preference fragment for ReVanced settings
*/
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void initialize() {
super.initialize();
// Do anything that forces this apps Settings bundle to load.
if (Settings.BLOCK_VIDEO_ADS.get()) {
Logger.printDebug(() -> "Block video ads enabled"); // Any statement that references the app settings.
}
}
}

View File

@ -1,4 +1,4 @@
package app.revanced.twitter.patches.hook.json
package app.revanced.integrations.twitter.patches.hook.json
import org.json.JSONObject

View File

@ -1,6 +1,6 @@
package app.revanced.twitter.patches.hook.json
package app.revanced.integrations.twitter.patches.hook.json
import app.revanced.twitter.patches.hook.patch.Hook
import app.revanced.integrations.twitter.patches.hook.patch.Hook
import org.json.JSONObject
interface JsonHook : Hook<JSONObject> {

View File

@ -1,8 +1,8 @@
package app.revanced.twitter.patches.hook.json
package app.revanced.integrations.twitter.patches.hook.json
import app.revanced.twitter.patches.hook.patch.dummy.DummyHook
import app.revanced.twitter.utils.json.JsonUtils.parseJson
import app.revanced.twitter.utils.stream.StreamUtils
import app.revanced.integrations.twitter.patches.hook.patch.dummy.DummyHook
import app.revanced.integrations.twitter.utils.json.JsonUtils.parseJson
import app.revanced.integrations.twitter.utils.stream.StreamUtils
import org.json.JSONException
import java.io.IOException
import java.io.InputStream

View File

@ -1,4 +1,4 @@
package app.revanced.twitter.patches.hook.patch
package app.revanced.integrations.twitter.patches.hook.patch
interface Hook<T> {
/**

View File

@ -1,7 +1,7 @@
package app.revanced.twitter.patches.hook.patch.ads
package app.revanced.integrations.twitter.patches.hook.patch.ads
import app.revanced.twitter.patches.hook.json.BaseJsonHook
import app.revanced.twitter.patches.hook.twifucker.TwiFucker
import app.revanced.integrations.twitter.patches.hook.json.BaseJsonHook
import app.revanced.integrations.twitter.patches.hook.twifucker.TwiFucker
import org.json.JSONObject

Some files were not shown because too many files have changed in this diff Show More