From 59687f1a39768ab71b2680a5c49df5aaae0d3b4c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 20 Oct 2023 03:13:37 +0200 Subject: [PATCH] feat(YouTube): Add `Announcements` patch (#503) --- .../announcements/AnnouncementsPatch.java | 151 ++++++++++++++++++ .../requests/AnnouncementsRoutes.java | 23 +++ .../patches/spoof/requests/PlayerRoutes.java | 8 +- .../integrations/requests/Requester.java | 3 +- .../integrations/settings/SettingsEnum.java | 9 +- .../integrations/utils/ReVancedUtils.java | 30 ++++ 6 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java create mode 100644 app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java diff --git a/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java b/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java new file mode 100644 index 00000000..e68d10c1 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/announcements/AnnouncementsPatch.java @@ -0,0 +1,151 @@ +package app.revanced.integrations.patches.announcements; + +import android.app.Activity; +import android.os.Build; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; +import androidx.annotation.RequiresApi; +import app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes; +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.UUID; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.integrations.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT; + +public final class AnnouncementsPatch { + private final static String CONSUMER = getOrSetConsumer(); + + private AnnouncementsPatch() { + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void showAnnouncement(final Activity context) { + if (!SettingsEnum.ANNOUNCEMENTS.getBoolean()) return; + + ReVancedUtils.runOnBackgroundThread(() -> { + try { + HttpURLConnection connection = AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT, CONSUMER); + + LogHelper.printDebug(() -> "Get latest announcement route connection url: " + connection.getURL().toString()); + + try { + // Do not show the announcement if the request failed. + if (connection.getResponseCode() != 200) { + if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return; + + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + ReVancedUtils.showToastLong("Failed to get announcement"); + + return; + } + } catch (IOException ex) { + final var message = "Failed connecting to announcements provider"; + + LogHelper.printException(() -> message, ex); + return; + } + + var jsonString = Requester.parseInputStreamAndClose(connection.getInputStream(), false); + + // Do not show the announcement if it is older or the same as the last one. + final byte[] hashBytes = MessageDigest.getInstance("SHA-256").digest(jsonString.getBytes(StandardCharsets.UTF_8)); + final var hash = java.util.Base64.getEncoder().encodeToString(hashBytes); + if (hash.equals(SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString())) return; + + // Parse the announcement. Fall-back to raw string if it fails. + String title; + String message; + Level level = Level.INFO; + try { + final var announcement = new JSONObject(jsonString); + + title = announcement.getString("title"); + message = announcement.getJSONObject("content").getString("message"); + + if (!announcement.isNull("level")) level = Level.fromInt(announcement.getInt("level")); + } catch (Throwable ex) { + LogHelper.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex); + + title = "Announcement"; + message = jsonString; + } + + final var finalTitle = title; + final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); + final Level finalLevel = level; + + ReVancedUtils.runOnMainThread(() -> { + // Show the announcement. + var alertDialog = new android.app.AlertDialog.Builder(context) + .setTitle(finalTitle) + .setMessage(finalMessage) + .setIcon(finalLevel.icon) + .setPositiveButton("Ok", (dialog, which) -> { + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(hash); + dialog.dismiss(); + }).setNegativeButton("Dismiss", (dialog, which) -> { + dialog.dismiss(); + }) + .setCancelable(false) + .show(); + + // Make links clickable. + ((TextView)alertDialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + }); + } catch (Exception e) { + final var message = "Failed to get announcement"; + + LogHelper.printException(() -> message, e); + } + }); + } + + /** + * Clears the last announcement hash if it is not empty. + * + * @return true if the last announcement hash was empty. + */ + private static boolean emptyLastAnnouncementHash() { + if (SettingsEnum.ANNOUNCEMENT_LAST_HASH.getString().isEmpty()) return true; + SettingsEnum.ANNOUNCEMENT_LAST_HASH.saveValue(""); + + return false; + } + + private static String getOrSetConsumer() { + final var consumer = SettingsEnum.ANNOUNCEMENT_CONSUMER.getString(); + if (!consumer.isEmpty()) return consumer; + + final var uuid = UUID.randomUUID().toString(); + SettingsEnum.ANNOUNCEMENT_CONSUMER.saveValue(uuid); + return uuid; + } + + // TODO: Use better icons. + private enum Level { + INFO(android.R.drawable.ic_dialog_info), + WARNING(android.R.drawable.ic_dialog_alert), + SEVERE(android.R.drawable.ic_dialog_alert); + + public final int icon; + + Level(int icon) { + this.icon = icon; + } + + public static Level fromInt(int value) { + return values()[Math.min(value, values().length - 1)]; + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java b/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java new file mode 100644 index 00000000..fc39090b --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/announcements/requests/AnnouncementsRoutes.java @@ -0,0 +1,23 @@ +package app.revanced.integrations.patches.announcements.requests; + +import app.revanced.integrations.requests.Requester; +import app.revanced.integrations.requests.Route; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import static app.revanced.integrations.requests.Route.Method.GET; + +public class AnnouncementsRoutes { + private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v2"; + + + public static final Route GET_LATEST_ANNOUNCEMENT = new Route(GET, "/announcements/youtube/latest?consumer={consumer}"); + + private AnnouncementsRoutes() { + } + + public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException { + return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java index db3971ba..a37dd6f3 100644 --- a/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/patches/spoof/requests/PlayerRoutes.java @@ -3,6 +3,7 @@ package app.revanced.integrations.patches.spoof.requests; import app.revanced.integrations.requests.Requester; import app.revanced.integrations.requests.Route; import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; import org.json.JSONException; import org.json.JSONObject; @@ -75,7 +76,12 @@ final class PlayerRoutes { /** @noinspection SameParameterValue*/ static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route) throws IOException { var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); - connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip"); + + connection.setRequestProperty( + "User-Agent", "com.google.android.youtube/" + + ReVancedUtils.getVersionName() + + " (Linux; U; Android 12; GB) gzip" + ); connection.setRequestProperty("X-Goog-Api-Format-Version", "2"); connection.setRequestProperty("Content-Type", "application/json"); diff --git a/app/src/main/java/app/revanced/integrations/requests/Requester.java b/app/src/main/java/app/revanced/integrations/requests/Requester.java index c756dfe8..0a49ecac 100644 --- a/app/src/main/java/app/revanced/integrations/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/requests/Requester.java @@ -1,5 +1,6 @@ package app.revanced.integrations.requests; +import app.revanced.integrations.utils.ReVancedUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -23,7 +24,7 @@ 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"); + connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + "; ReVanced/" + ReVancedUtils.getVersionName()); return connection; } diff --git a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java index ac1605af..a6f47755 100644 --- a/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java +++ b/app/src/main/java/app/revanced/integrations/settings/SettingsEnum.java @@ -10,10 +10,7 @@ import app.revanced.integrations.utils.StringRef; import org.json.JSONException; import org.json.JSONObject; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static app.revanced.integrations.settings.SettingsEnum.ReturnType.*; import static app.revanced.integrations.settings.SharedPrefCategory.RETURN_YOUTUBE_DISLIKE; @@ -179,6 +176,9 @@ public enum SettingsEnum { 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, ""), // Swipe controls SWIPE_BRIGHTNESS("revanced_swipe_brightness", BOOLEAN, TRUE), @@ -555,6 +555,7 @@ public enum SettingsEnum { 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: diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index 286a03bf..918d55b7 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -2,8 +2,11 @@ package app.revanced.integrations.utils; import android.annotation.SuppressLint; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; @@ -25,9 +28,36 @@ public class ReVancedUtils { @SuppressLint("StaticFieldLeak") public static Context context; + private static String versionName; + private ReVancedUtils() { } // utility class + public static String getVersionName() { + if (versionName != null) return versionName; + + PackageInfo packageInfo; + try { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + packageInfo = context.getPackageManager().getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + else + packageInfo = context.getPackageManager().getPackageInfo( + packageName, + 0 + ); + } catch (PackageManager.NameNotFoundException e) { + LogHelper.printException(() -> "Failed to get package info", e); + return null; + } + + return versionName = packageInfo.versionName; + } + /** * Hide a view by setting its layout height and width to 1dp. *