feat(YouTube): Add `Announcements` patch (#503)

This commit is contained in:
oSumAtrIX 2023-10-20 03:13:37 +02:00 committed by GitHub
parent cd6ba256a5
commit 59687f1a39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 218 additions and 6 deletions

View File

@ -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)];
}
}
}

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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;
}

View File

@ -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:

View File

@ -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.
*