diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 371100d1..bdad852f 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -35,6 +35,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import app.revanced.integrations.shared.settings.BooleanSetting; +import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference; import kotlin.text.Regex; public class Utils { @@ -47,30 +48,44 @@ public class Utils { private Utils() { } // utility class - public static String getVersionName() { - if (versionName != null) return versionName; + /** + * Injection point. + * + * @return The manifest 'Version' entry of the patches.jar used during patching. + */ + public static String getPatchesReleaseVersion() { + return ""; // Value is replaced during patching. + } - PackageInfo packageInfo; - try { - final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + /** + * @return The version name of the app, such as "YouTube". + */ + public static String getAppVersionName() { + if (versionName == null) { + try { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); - PackageManager packageManager = context.getPackageManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - packageInfo = packageManager.getPackageInfo( - packageName, - PackageManager.PackageInfoFlags.of(0) - ); - else - packageInfo = packageManager.getPackageInfo( - packageName, - 0 - ); - } catch (PackageManager.NameNotFoundException e) { - Logger.printException(() -> "Failed to get package info", e); - return null; + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageInfo = packageManager.getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + } else { + packageInfo = packageManager.getPackageInfo( + packageName, + 0 + ); + } + versionName = packageInfo.versionName; + } catch (Exception ex) { + Logger.printException(() -> "Failed to get package info", ex); + versionName = "Unknown"; + } } - return versionName = packageInfo.versionName; + return versionName; } /** @@ -185,6 +200,7 @@ public class Utils { } public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + //noinspection deprecation return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); } @@ -323,7 +339,7 @@ public class Utils { try { runnable.run(); } catch (Exception ex) { - Logger.printException(() -> runnable.getClass() + ": " + ex.getMessage(), ex); + Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex); } }; new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis); @@ -445,11 +461,8 @@ public class Utils { this.keySuffix = keySuffix; } - /** - * Defaults to {@link #UNSORTED} if key is null or has no sort suffix. - */ @NonNull - static Sort fromKey(@Nullable String key) { + static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) { if (key != null) { for (Sort sort : values()) { if (key.endsWith(sort.keySuffix)) { @@ -457,7 +470,7 @@ public class Utils { } } } - return UNSORTED; + return defaultSort; } } @@ -479,19 +492,26 @@ public class Utils { * If a preference has no key or no {@link Sort} suffix, * then the preferences are left unsorted. */ + @SuppressWarnings("deprecation") public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { - Sort sort = Sort.fromKey(group.getKey()); + Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); SortedMap preferences = new TreeMap<>(); for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { Preference preference = group.getPreference(i); + final Sort preferenceSort; if (preference instanceof PreferenceGroup) { sortPreferenceGroups((PreferenceGroup) preference); + preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. + } else { + // Allow individual preferences to set a key sorting. + // Used to force a preference to the top or bottom of a group. + preferenceSort = Sort.fromKey(preference.getKey(), groupSort); } final String sortValue; - switch (sort) { + switch (preferenceSort) { case BY_TITLE: sortValue = removePunctuationConvertToLowercase(preference.getTitle()); break; @@ -511,8 +531,9 @@ public class Utils { for (Preference pref : preferences.values()) { int order = index++; - // If the preference is a PreferenceScreen or is an intent preference, move to the top. - if (pref instanceof PreferenceScreen || pref.getIntent() != null) { + // Move any screens, intents, and the one off About preference to the top. + if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference + || pref.getIntent() != null) { // Arbitrary high number. order -= 1000; } diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java index 34a348a3..b7fa68ac 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java @@ -16,10 +16,7 @@ import app.revanced.integrations.shared.settings.Setting; import static app.revanced.integrations.shared.StringRef.str; -/** - * - * - * @noinspection deprecation, DataFlowIssue , unused */ +@SuppressWarnings({"unused", "deprecation"}) public abstract class AbstractPreferenceFragment extends PreferenceFragment { /** * Indicates that if a preference changes, diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java index 0a9b54c1..5c8e7c9b 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java @@ -15,7 +15,7 @@ import app.revanced.integrations.shared.Utils; import static app.revanced.integrations.shared.StringRef.str; -/** @noinspection deprecation, unused */ +@SuppressWarnings({"unused", "deprecation"}) public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { private String existingSettings; diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java new file mode 100644 index 00000000..f5911a01 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ReVancedAboutPreference.java @@ -0,0 +1,312 @@ +package app.revanced.integrations.shared.settings.preference; + +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.requests.Route.Method.GET; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.requests.Requester; +import app.revanced.integrations.youtube.requests.Route; + +/** + * Opens a dialog showing the links from {@link SocialLinksRoutes}. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ReVancedAboutPreference extends Preference { + + private static String useNonBreakingHyphens(String text) { + // Replace any dashes with non breaking dashes, so the English text 'pre-release' + // and the dev release number does not break and cover two lines. + return text.replace("-", "‑"); // #8209 = non breaking hyphen. + } + + private static String getColorHexString(int color) { + return String.format("#%06X", (0x00FFFFFF & color)); + } + + protected boolean isDarkModeEnabled() { + Configuration config = getContext().getResources().getConfiguration(); + final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getLightColor() { + return Color.WHITE; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getDarkColor() { + return Color.BLACK; + } + + private String createDialogHtml(ReVancedSocialLink[] socialLinks) { + final boolean isNetworkConnected = Utils.isNetworkConnected(); + + StringBuilder builder = new StringBuilder(); + builder.append(""); + builder.append(""); + + final boolean isDarkMode = isDarkModeEnabled(); + String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor()); + String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor()); + // Apply light/dark mode colors. + builder.append(String.format( + "", + backgroundColorHex, foregroundColorHex, foregroundColorHex)); + + if (isNetworkConnected) { + builder.append(""); + } + + String patchesVersion = Utils.getPatchesReleaseVersion(); + + // Add the title. + builder.append("

") + .append("ReVanced") + .append("

"); + + builder.append("

") + // Replace hyphens with non breaking dashes so the version number does not break lines. + .append(useNonBreakingHyphens(str("revanced_settings_about_links_body", patchesVersion))) + .append("

"); + + // Add a disclaimer if using a dev release. + if (patchesVersion.contains("dev")) { + builder.append("

") + // English text 'Pre-release' can break lines. + .append(useNonBreakingHyphens(str("revanced_settings_about_links_dev_header"))) + .append("

"); + + builder.append("

") + .append(str("revanced_settings_about_links_dev_body")) + .append("

"); + } + + builder.append("

") + .append(str("revanced_settings_about_links_header")) + .append("

"); + + builder.append("
"); + for (ReVancedSocialLink social : socialLinks) { + builder.append("
"); + builder.append(String.format("%s", social.url, social.name)); + builder.append("
"); + } + builder.append("
"); + + builder.append(""); + return builder.toString(); + } + + { + setOnPreferenceClickListener(pref -> { + // Show a progress spinner if the social links are not fetched yet. + if (!SocialLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) { + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progress.show(); + Utils.runOnBackgroundThread(() -> fetchLinksAndShowDialog(progress)); + } else { + // No network call required and can run now. + fetchLinksAndShowDialog(null); + } + + return false; + }); + } + + private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) { + ReVancedSocialLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks(); + String htmlDialog = createDialogHtml(socialLinks); + + Utils.runOnMainThreadNowOrLater(() -> { + if (progress != null) { + progress.dismiss(); + } + new WebViewDialog(getContext(), htmlDialog).show(); + }); + } + + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedAboutPreference(Context context) { + super(context); + } +} + +/** + * Displays html content as a dialog. Any links a user taps on are opened in an external browser. + */ +class WebViewDialog extends Dialog { + + private final String htmlContent; + + public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) { + super(context); + this.htmlContent = htmlContent; + } + + // JS required to hide any broken images. No remote javascript is ever loaded. + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + WebView webView = new WebView(getContext()); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new OpenLinksExternallyWebClient()); + webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); + + setContentView(webView); + } + + private class OpenLinksExternallyWebClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + getContext().startActivity(intent); + } catch (Exception ex) { + Logger.printException(() -> "Open link failure", ex); + } + // Dismiss the about dialog using a delay, + // otherwise without a delay the UI looks hectic with the dialog dismissing + // to show the settings while simultaneously a web browser is opening. + Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500); + return true; + } + } +} + +class ReVancedSocialLink { + final boolean preferred; + final String name; + final String url; + + ReVancedSocialLink(JSONObject json) throws JSONException { + this(json.getBoolean("preferred"), + json.getString("name"), + json.getString("url") + ); + } + + ReVancedSocialLink(boolean preferred, String name, String url) { + this.preferred = preferred; + this.name = name; + this.url = url; + } + + @NonNull + @Override + public String toString() { + return "ReVancedSocialLink{" + + "preferred=" + preferred + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + '}'; + } +} + +class SocialLinksRoutes { + /** + * Links to use if fetch links api call fails. + */ + private static final ReVancedSocialLink[] NO_CONNECTION_STATIC_LINKS = { + new ReVancedSocialLink(true, "ReVanced.app", "https://revanced.app") + }; + + private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2"; + private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile(); + + @Nullable + private static volatile ReVancedSocialLink[] fetchedLinks; + + static boolean hasFetchedLinks() { + return fetchedLinks != null; + } + + static ReVancedSocialLink[] fetchSocialLinks() { + try { + if (hasFetchedLinks()) return fetchedLinks; + + // Check if there is no internet connection. + if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS; + + HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + Logger.printDebug(() -> "Fetching social links from: " + connection.getURL()); + + // Do not show an exception toast if the server is down + final int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode); + return NO_CONNECTION_STATIC_LINKS; + } + + JSONObject json = Requester.parseJSONObjectAndDisconnect(connection); + JSONArray socials = json.getJSONArray("socials"); + + List links = new ArrayList<>(); + for (int i = 0, length = socials.length(); i < length; i++) { + ReVancedSocialLink link = new ReVancedSocialLink(socials.getJSONObject(i)); + links.add(link); + } + Logger.printDebug(() -> "links: " + links); + + return fetchedLinks = links.toArray(new ReVancedSocialLink[0]); + + } catch (SocketTimeoutException ex) { + Logger.printInfo(() -> "Could not fetch social links", ex); // No toast. + } catch (JSONException ex) { + Logger.printException(() -> "Could not parse about information", ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to get about information", ex); + } + + return NO_CONNECTION_STATIC_LINKS; + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java index 804b51c4..4cf1f277 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java @@ -14,7 +14,7 @@ import java.util.Objects; import static app.revanced.integrations.shared.StringRef.str; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "deprecation"}) 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); - Setting setting = Setting.getSettingFromPath(getKey()); + Setting setting = Setting.getSettingFromPath(getKey()); if (setting != null) { builder.setNeutralButton(str("revanced_settings_reset"), null); } @@ -50,7 +50,7 @@ public class ResettableEditTextPreference extends EditTextPreference { } button.setOnClickListener(v -> { try { - Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); + Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); String defaultStringValue = setting.defaultValue.toString(); EditText editText = getEditText(); editText.setText(defaultStringValue); diff --git a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java index 0faa46b1..0cc8061c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java +++ b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java @@ -1,14 +1,23 @@ package app.revanced.integrations.youtube; import android.app.Activity; +import android.graphics.Color; + +import androidx.annotation.Nullable; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; public class ThemeHelper { + @Nullable + private static Integer darkThemeColor, lightThemeColor; private static int themeValue; - public static void setTheme(Object value) { - final int newOrdinalValue = ((Enum) value).ordinal(); + /** + * Injection point. + */ + public static void setTheme(Enum value) { + final int newOrdinalValue = value.ordinal(); if (themeValue != newOrdinalValue) { themeValue = newOrdinalValue; Logger.printDebug(() -> "Theme value: " + newOrdinalValue); @@ -26,4 +35,48 @@ public class ThemeHelper { activity.setTheme(Utils.getResourceIdentifier(theme, "style")); } + /** + * Injection point. + */ + private static String darkThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@android:color/black"; + } + + /** + * @return The dark theme color as specified by the Theme patch (if included), + * or the Android color of black. + */ + public static int getDarkThemeColor() { + if (darkThemeColor == null) { + darkThemeColor = getColorInt(darkThemeResourceName()); + } + return darkThemeColor; + } + + /** + * Injection point. + */ + private static String lightThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@android:color/white"; + } + + /** + * @return The light theme color as specified by the Theme patch (if included), + * or the Android color of white. + */ + public static int getLightThemeColor() { + if (lightThemeColor == null) { + lightThemeColor = getColorInt(lightThemeResourceName()); + } + return lightThemeColor; + } + + private static int getColorInt(String colorString) { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString); + } + return Utils.getResourceColor(colorString); + } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java index db1e3755..eec599ec 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/announcements/AnnouncementsPatch.java @@ -61,7 +61,7 @@ public final class AnnouncementsPatch { return; } - var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false); + var jsonString = Requester.parseStringAndDisconnect(connection); // Parse the announcement. Fall-back to raw string if it fails. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java index 67e69502..7909387f 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/spoof/requests/PlayerRoutes.java @@ -37,7 +37,7 @@ final class PlayerRoutes { JSONObject client = new JSONObject(); client.put("clientName", "ANDROID"); - client.put("clientVersion", Utils.getVersionName()); + client.put("clientVersion", Utils.getAppVersionName()); client.put("androidSdkVersion", 34); context.put("client", client); @@ -85,7 +85,7 @@ final class PlayerRoutes { connection.setRequestProperty( "User-Agent", "com.google.android.youtube/" + - Utils.getVersionName() + + Utils.getAppVersionName() + " (Linux; U; Android 12; GB) gzip" ); connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java index 5c109f98..ef409b52 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java @@ -24,7 +24,10 @@ public class Requester { String url = apiUrl + route.getCompiledRoute(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod(route.getMethod().name()); - connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + Utils.getVersionName()); + String agentString = System.getProperty("http.agent") + + "; ReVanced/" + Utils.getAppVersionName() + + " (" + Utils.getPatchesReleaseVersion() + ")"; + connection.setRequestProperty("User-Agent", agentString); return connection; } @@ -32,72 +35,79 @@ public class Requester { /** * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. */ - public static String parseJson(HttpURLConnection connection) throws IOException { - return parseInputStreamAndClose(connection.getInputStream(), true); - } - - /** - * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. - * - * Should only be used if other requests to the server are unlikely in the near future - * - * @see #parseJson(HttpURLConnection) - */ - public static String parseJsonAndDisconnect(HttpURLConnection connection) throws IOException { - String result = parseJson(connection); - connection.disconnect(); - return result; - } - - /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. - * - * @param stripNewLineCharacters if newline (\n) characters should be stripped from the InputStream - */ - public static String parseInputStreamAndClose(InputStream inputStream, boolean stripNewLineCharacters) throws IOException { + private static String parseInputStreamAndClose(InputStream inputStream) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { StringBuilder jsonBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { jsonBuilder.append(line); - if (!stripNewLineCharacters) - jsonBuilder.append("\n"); + jsonBuilder.append("\n"); } return jsonBuilder.toString(); } } /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * Parse the {@link HttpURLConnection} response as a String. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}. */ - public static String parseErrorJson(HttpURLConnection connection) throws IOException { - return parseInputStreamAndClose(connection.getErrorStream(), false); + public static String parseString(HttpURLConnection connection) throws IOException { + return parseInputStreamAndClose(connection.getInputStream()); } /** - * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * Parse the {@link HttpURLConnection} response as a String, and disconnect. * - * Should only be used if other requests to the server are unlikely in the near future + * Should only be used if other requests to the server in the near future are unlikely * - * @see #parseErrorJson(HttpURLConnection) + * @see #parseString(HttpURLConnection) */ - public static String parseErrorJsonAndDisconnect(HttpURLConnection connection) throws IOException { - String result = parseErrorJson(connection); + public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseString(connection); connection.disconnect(); return result; } /** - * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * Parse the {@link HttpURLConnection} error stream as a String. + * If the server sent no error response data, this returns an empty string. + */ + public static String parseErrorString(HttpURLConnection connection) throws IOException { + InputStream errorStream = connection.getErrorStream(); + if (errorStream == null) { + return ""; + } + return parseInputStreamAndClose(errorStream); + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String, and disconnect. + * If the server sent no error response data, this returns an empty string. + * + * Should only be used if other requests to the server are unlikely in the near future. + * + * @see #parseErrorString(HttpURLConnection) + */ + public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} response into a JSONObject. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}. */ public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { - return new JSONObject(parseJson(connection)); + return new JSONObject(parseString(connection)); } /** * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. * - * Should only be used if other requests to the server are unlikely in the near future + * Should only be used if other requests to the server in the near future are unlikely * * @see #parseJSONObject(HttpURLConnection) */ @@ -109,15 +119,17 @@ public class Requester { /** * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}. */ public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { - return new JSONArray(parseJson(connection)); + return new JSONArray(parseString(connection)); } /** * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. * - * Should only be used if other requests to the server are unlikely in the near future + * Should only be used if other requests to the server in the near future are unlikely * * @see #parseJSONArray(HttpURLConnection) */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index 9b22aafc..bc729e47 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -1,7 +1,7 @@ package app.revanced.integrations.youtube.returnyoutubedislike.requests; -import static app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; import android.util.Base64; @@ -22,11 +22,11 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Objects; +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.requests.Requester; import app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.youtube.settings.Settings; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; public class ReturnYouTubeDislikeApi { /** @@ -383,7 +383,7 @@ public class ReturnYouTubeDislikeApi { } // Something went wrong, might as well disconnect. - String response = Requester.parseJsonAndDisconnect(connection); + String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm registration for user: " + userId + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), @@ -505,7 +505,7 @@ public class ReturnYouTubeDislikeApi { } // Something went wrong, might as well disconnect. - String response = Requester.parseJsonAndDisconnect(connection); + String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java index b631e2e1..f0153e7e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -9,7 +9,7 @@ import android.util.AttributeSet; /** * Allows tapping the DeArrow about preference to open the DeArrow website. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "deprecation"}) public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { { setOnPreferenceClickListener(pref -> { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java new file mode 100644 index 00000000..33601651 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedYouTubeAboutPreference.java @@ -0,0 +1,32 @@ +package app.revanced.integrations.youtube.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; + +import app.revanced.integrations.shared.settings.preference.ReVancedAboutPreference; +import app.revanced.integrations.youtube.ThemeHelper; + +@SuppressWarnings("unused") +public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference { + + public int getLightColor() { + return ThemeHelper.getLightThemeColor(); + } + + public int getDarkColor() { + return ThemeHelper.getDarkThemeColor(); + } + + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedYouTubeAboutPreference(Context context) { + super(context); + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java index 020fd038..9e25e9f6 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/requests/SBRequester.java @@ -159,13 +159,13 @@ public class SBRequester { messageToToast = str("revanced_sb_submit_failed_duplicate"); break; case 403: - messageToToast = str("revanced_sb_submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("revanced_sb_submit_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection)); break; case 429: messageToToast = str("revanced_sb_submit_failed_rate_limit"); break; case 400: - messageToToast = str("revanced_sb_submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection)); + messageToToast = str("revanced_sb_submit_failed_invalid", Requester.parseErrorStringAndDisconnect(connection)); break; default: messageToToast = str("revanced_sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage()); @@ -223,7 +223,7 @@ public class SBRequester { break; case 403: Utils.showToastLong( - str("revanced_sb_vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection))); + str("revanced_sb_vote_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection))); break; default: Utils.showToastLong(