feat: Add Check environment patch (#683)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2024-09-06 12:27:02 +04:00 committed by GitHub
parent a324b16096
commit e856455283
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 703 additions and 46 deletions

View File

@ -54,18 +54,15 @@ public class GmsCoreSupport {
String dialogMessageRef, String dialogMessageRef,
String positiveButtonStringRef, String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) { DialogInterface.OnClickListener onPositiveClickListener) {
// Use a delay to allow the activity to finish initializing. // Do not set cancelable to false, to allow using back button to skip the action,
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme. // just in case the check can never be satisfied.
Utils.runOnMainThreadDelayed(() -> { var dialog = new AlertDialog.Builder(context)
new AlertDialog.Builder(context) .setIconAttribute(android.R.attr.alertDialogIcon)
.setIconAttribute(android.R.attr.alertDialogIcon) .setTitle(str("gms_core_dialog_title"))
.setTitle(str("gms_core_dialog_title")) .setMessage(str(dialogMessageRef))
.setMessage(str(dialogMessageRef)) .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) .create();
// Allow using back button to skip the action, just in case the check can never be satisfied. Utils.showDialog(context, dialog);
.setCancelable(true)
.show();
}, 100);
} }
/** /**

View File

@ -1,24 +1,21 @@
package app.revanced.integrations.shared; 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 android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import app.revanced.integrations.shared.settings.BaseSettings;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import app.revanced.integrations.shared.settings.BaseSettings; import static app.revanced.integrations.shared.settings.BaseSettings.*;
public class Logger { public class Logger {
/** /**
* Log messages using lambdas. * Log messages using lambdas.
*/ */
@FunctionalInterface
public interface LogMessage { public interface LogMessage {
@NonNull @NonNull
String buildMessageString(); String buildMessageString();
@ -59,19 +56,33 @@ public class Logger {
* so the performance cost of building strings is paid only if {@link BaseSettings#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) { public static void printDebug(@NonNull LogMessage message) {
printDebug(message, null);
}
/**
* 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 BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) { if (DEBUG.get()) {
var messageString = message.buildMessageString(); String logMessage = message.buildMessageString();
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
if (DEBUG_STACKTRACE.get()) { if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(messageString); var builder = new StringBuilder(logMessage);
var sw = new StringWriter(); var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw)); new Throwable().printStackTrace(new PrintWriter(sw));
builder.append('\n').append(sw); builder.append('\n').append(sw);
messageString = builder.toString(); logMessage = builder.toString();
} }
Log.d(REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(), messageString); if (ex == null) {
Log.d(logTag, logMessage);
} else {
Log.d(logTag, logMessage, ex);
}
} }
} }

View File

@ -1,6 +1,10 @@
package app.revanced.integrations.shared; package app.revanced.integrations.shared;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
@ -8,6 +12,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.preference.Preference; import android.preference.Preference;
@ -380,6 +385,82 @@ public class Utils {
return false; return false;
} }
/**
* Ignore this class. It must be public to satisfy Android requirement.
*/
@SuppressWarnings("deprecation")
public static class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
@SuppressWarnings("deprecation")
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/** /**
* Safe to call from any thread * Safe to call from any thread
*/ */

View File

@ -0,0 +1,164 @@
package app.revanced.integrations.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.DialogFragmentOnStartAction;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.text.Html;
import android.widget.Button;
import androidx.annotation.Nullable;
import java.util.Collection;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
/**
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
*/
@Nullable
protected abstract Boolean check();
protected abstract String failureReason();
/**
* Specifies a sorting order for displaying the checks that failed.
* A lower value indicates to show first before other checks.
*/
public abstract int uiSortingValue();
/**
* For debugging and development only.
* Forces all checks to be performed and the check failed dialog to be shown.
* Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
* set to -1.
*/
static boolean debugAlwaysShowWarning() {
final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
if (alwaysShowWarning) {
Logger.printInfo(() -> "Debug forcing environment check warning to show");
}
return alwaysShowWarning;
}
static boolean shouldRun() {
return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
}
static void disableForever() {
Logger.printInfo(() -> "Environment checks disabled forever");
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();
reasons.append("<ul>");
for (var check : failedChecks) {
// Add a non breaking space to fix bullet points spacing issue.
reasons.append("<li>&nbsp;").append(check.failureReason());
}
reasons.append("</ul>");
var message = Html.fromHtml(
str("revanced_check_environment_failed_message", reasons.toString()),
FROM_HTML_MODE_COMPACT
);
Utils.runOnMainThreadDelayed(() -> {
AlertDialog alert = new AlertDialog.Builder(activity)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("revanced_check_environment_failed_title"))
.setMessage(message)
.setPositiveButton(
" ",
(dialog, which) -> {
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
// Shutdown to prevent the user from navigating back to this app,
// which is no longer showing a warning dialog.
activity.finishAffinity();
System.exit(0);
}
).setNegativeButton(
" ",
(dialog, which) -> {
// Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
dialog.dismiss();
}
).create();
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(AlertDialog dialog) {
// Only run this once, otherwise if the user changes to a different app
// then changes back, this handler will run again and disable the buttons.
if (hasRun) {
return;
}
hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
openWebsiteButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
dismissButton.setEnabled(false);
getCountdownRunnable(dismissButton, openWebsiteButton).run();
}
});
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
}
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@Override
public void run() {
Utils.verifyOnMainThread();
if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
dismissButton.setEnabled(true);
}
}
};
}
}

View File

@ -0,0 +1,369 @@
package app.revanced.integrations.shared.checks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.checks.Check.debugAlwaysShowWarning;
import static app.revanced.integrations.shared.checks.PatchInfo.Build.*;
import static app.revanced.integrations.shared.checks.PatchInfo.PATCH_TIME;
/**
* This class is used to check if the app was patched by the user
* and not downloaded pre-patched, because pre-patched apps are difficult to trust.
* <br>
* Various indicators help to detect if the app was patched by the user.
*/
@SuppressWarnings("unused")
public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
private enum InstallationType {
/**
* CLI patching, manual installation of a previously patched using adb,
* or root installation if stock app is first installed using adb.
*/
ADB((String) null),
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
MANAGER("app.revanced.manager.flutter",
"app.revanced.manager",
"app.revanced.manager.debug");
@Nullable
static InstallationType installTypeFromPackageName(@Nullable String packageName) {
for (InstallationType type : values()) {
for (String installPackageName : type.packageNames) {
if (Objects.equals(installPackageName, packageName)) {
return type;
}
}
}
return null;
}
/**
* Array elements can be null.
*/
final String[] packageNames;
InstallationType(String... packageNames) {
this.packageNames = packageNames;
}
}
/**
* Check if the app is installed by the manager, the app store, or through adb/CLI.
* <br>
* Does not conclusively
* If the app is installed by the manager or the app store, it is likely, the app was patched using the manager,
* or installed manually via ADB (in the case of ReVanced CLI for example).
* <br>
* If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched
* and installed by the browser or another unknown app.
*/
private static class CheckExpectedInstaller extends Check {
@Nullable
InstallationType installerFound;
@NonNull
@Override
protected Boolean check() {
final var context = Utils.getContext();
final var installerPackageName =
context.getPackageManager().getInstallerPackageName(context.getPackageName());
Logger.printInfo(() -> "Installed by: " + installerPackageName);
installerFound = InstallationType.installTypeFromPackageName(installerPackageName);
final boolean passed = (installerFound != null);
Logger.printInfo(() -> passed
? "Apk was not installed from an unknown source"
: "Apk was installed from an unknown source");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_manager_not_expected_installer");
}
@Override
public int uiSortingValue() {
return -100; // Show first.
}
}
/**
* Check if the build properties are the same as during the patch.
* <br>
* If the build properties are the same as during the patch, it is likely, the app was patched on the same device.
* <br>
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/
private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint({"NewApi", "HardwareIds"})
@Override
protected Boolean check() {
if (PATCH_BOARD.isEmpty()) {
// Did not patch with Manager, and cannot conclusively say where this was from.
Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device");
return null;
}
//noinspection deprecation
final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) &
buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) &
buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) &
buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) &
buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) &
buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) &
buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) &
buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) &
buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) &
buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) &
buildFieldEqualsHash("ID", Build.ID, PATCH_ID) &
buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) &
buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) &
buildFieldEqualsHash("ODM_SKU", Build.ODM_SKU, PATCH_ODM_SKU) &
buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) &
buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) &
buildFieldEqualsHash("SKU", Build.SKU, PATCH_SKU) &
buildFieldEqualsHash("SOC_MANUFACTURER", Build.SOC_MANUFACTURER, PATCH_SOC_MANUFACTURER) &
buildFieldEqualsHash("SOC_MODEL", Build.SOC_MODEL, PATCH_SOC_MODEL) &
buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) &
buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) &
buildFieldEqualsHash("USER", Build.USER, PATCH_USER);
Logger.printInfo(() -> passed
? "Device hardware signature matches current device"
: "Device hardware signature does not match current device");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_not_same_patching_device");
}
@Override
public int uiSortingValue() {
return 0; // Show in the middle.
}
}
/**
* Check if the app was installed within the last 30 minutes after being patched.
* <br>
* If the app was installed within the last 30 minutes, it is likely, the app was patched by the user.
* <br>
* If the app was installed much later than the patch time, it is likely the app was
* downloaded pre-patched or the user waited too long to install the app.
*/
private static class CheckIsNearPatchTime extends Check {
/**
* How soon after patching the app must be first launched.
*/
static final int THRESHOLD_FOR_PATCHING_RECENTLY = 30 * 60 * 1000; // 30 minutes.
/**
* How soon after installation or updating the app to check the patch time.
* If the install/update is older than this, this entire check is ignored
* to prevent showing any errors if the user clears the app data after installation.
*/
static final int THRESHOLD_FOR_RECENT_INSTALLATION = 12 * 60 * 60 * 1000; // 12 hours.
static final long DURATION_SINCE_PATCHING = System.currentTimeMillis() - PATCH_TIME;
@Override
protected Boolean check() {
Logger.printInfo(() -> "Installed: " + (DURATION_SINCE_PATCHING / 1000) + " seconds after patching");
// Also verify patched time is not in the future.
if (DURATION_SINCE_PATCHING < 0) {
// Patch time is in the future and clearly wrong.
return false;
}
if (DURATION_SINCE_PATCHING < THRESHOLD_FOR_PATCHING_RECENTLY) {
// App is recently patched and this installation is new or recently updated.
return true;
}
// Verify the app install/update is recent,
// to prevent showing errors if the user later clears the app data.
try {
Context context = Utils.getContext();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
// Duration since initial install or last update, which ever is sooner.
final long durationSinceInstallUpdate = System.currentTimeMillis() - packageInfo.lastUpdateTime;
Logger.printInfo(() -> "App was installed/updated: "
+ (durationSinceInstallUpdate / (60 * 60 * 1000)) + " hours ago");
if (durationSinceInstallUpdate > THRESHOLD_FOR_RECENT_INSTALLATION) {
Logger.printInfo(() -> "Ignoring install time check since install/update was over "
+ THRESHOLD_FOR_RECENT_INSTALLATION + " hours ago");
return null;
}
} catch (PackageManager.NameNotFoundException ex) {
Logger.printException(() -> "Package name not found exception", ex); // Will never happen.
}
// Was patched between 30 minutes and 12 hours ago.
// This can only happen if someone installs the app then waits 30+ minutes to launch,
// or they clear the app data within 12 hours after installation.
return false;
}
@Override
protected String failureReason() {
if (DURATION_SINCE_PATCHING < 0) {
// Could happen if the user has their device clock incorrectly set in the past,
// but assume that isn't the case and the apk was patched on a device with the wrong system time.
return str("revanced_check_environment_not_near_patch_time_invalid");
}
// If patched over 1 day ago, show how old this pre-patched apk is.
// Showing the age can help convey it's better to patch yourself and know it's the latest.
final long oneDay = 24 * 60 * 60 * 1000;
final long daysSincePatching = DURATION_SINCE_PATCHING / oneDay;
if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings.
return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching);
}
return str("revanced_check_environment_not_near_patch_time");
}
@Override
public int uiSortingValue() {
return 100; // Show last.
}
}
/**
* Injection point.
*/
public static void check(Activity context) {
// If the warning was already issued twice, or if the check was successful in the past,
// do not run the checks again.
if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
Logger.printDebug(() -> "Environment checks are disabled");
return;
}
Utils.runOnBackgroundThread(() -> {
try {
Logger.printInfo(() -> "Running environment checks");
List<Check> failedChecks = new ArrayList<>();
CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice();
Boolean hardwareCheckPassed = sameHardware.check();
if (hardwareCheckPassed != null) {
if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Patched on the same device using Manager,
// and no further checks are needed.
Check.disableForever();
return;
}
failedChecks.add(sameHardware);
}
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed != null) {
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
if (failedChecks.isEmpty()) {
// Recently patched and installed. No further checks are needed.
// Stopping here also prevents showing warnings if patching and installing with Termux.
Check.disableForever();
return;
}
} else {
failedChecks.add(nearPatchTime);
}
}
CheckExpectedInstaller installerCheck = new CheckExpectedInstaller();
// If the installer package is Manager but this code is reached,
// that means it must not be the right Manager otherwise the hardware hash
// signatures would be present and this check would not have run.
final boolean isManagerInstall = installerCheck.installerFound == InstallationType.MANAGER;
if (!installerCheck.check() || isManagerInstall) {
failedChecks.add(installerCheck);
if (isManagerInstall) {
// If using Manager and reached here, then this must
// have been patched on a different device.
failedChecks.add(sameHardware);
}
}
if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Show all failures for debugging layout.
failedChecks = Arrays.asList(
sameHardware,
nearPatchTime,
installerCheck
);
}
if (failedChecks.isEmpty()) {
Check.disableForever();
return;
}
//noinspection ComparatorCombinators
Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue());
Check.issueWarning(
context,
failedChecks
);
} catch (Exception ex) {
Logger.printException(() -> "check failure", ex);
}
});
}
private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) {
try {
final var sha1 = MessageDigest.getInstance("SHA-1")
.digest(buildFieldValue.getBytes(StandardCharsets.UTF_8));
// Must be careful to use same base64 encoding Kotlin uses.
String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1);
final boolean equals = runtimeHash.equals(hash);
if (!equals) {
Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue
+ "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'");
}
return equals;
} catch (NoSuchAlgorithmException ex) {
Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen.
return false;
}
}
}

View File

@ -0,0 +1,33 @@
package app.revanced.integrations.shared.checks;
// Fields are set by the patch. Do not modify.
// Fields are not final, because the compiler is inlining them.
final class PatchInfo {
static long PATCH_TIME = 0L;
final static class Build {
static String PATCH_BOARD = "";
static String PATCH_BOOTLOADER = "";
static String PATCH_BRAND = "";
static String PATCH_CPU_ABI = "";
static String PATCH_CPU_ABI2 = "";
static String PATCH_DEVICE = "";
static String PATCH_DISPLAY = "";
static String PATCH_FINGERPRINT = "";
static String PATCH_HARDWARE = "";
static String PATCH_HOST = "";
static String PATCH_ID = "";
static String PATCH_MANUFACTURER = "";
static String PATCH_MODEL = "";
static String PATCH_ODM_SKU = "";
static String PATCH_PRODUCT = "";
static String PATCH_RADIO = "";
static String PATCH_SERIAL = "";
static String PATCH_SKU = "";
static String PATCH_SOC_MANUFACTURER = "";
static String PATCH_SOC_MODEL = "";
static String PATCH_TAGS = "";
static String PATCH_TYPE = "";
static String PATCH_USER = "";
}
}

View File

@ -55,7 +55,7 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
} }
Utils.runOnMainThread(() -> { Utils.runOnMainThread(() -> {
var alertDialog = new android.app.AlertDialog.Builder(context) var alert = new android.app.AlertDialog.Builder(context)
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title")) .setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) .setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
.setIconAttribute(android.R.attr.alertDialogIcon) .setIconAttribute(android.R.attr.alertDialogIcon)
@ -64,9 +64,9 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { }).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
dialog.dismiss(); dialog.dismiss();
}) }).create();
.setCancelable(false)
.show(); Utils.showDialog(context, alert, false, null);
}); });
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver failure", ex); Logger.printException(() -> "checkDnsResolver failure", ex);

View File

@ -1,6 +1,7 @@
package app.revanced.integrations.youtube.patches.announcements; package app.revanced.integrations.youtube.patches.announcements;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.os.Build; import android.os.Build;
import android.text.Html; import android.text.Html;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
@ -103,8 +104,6 @@ public final class AnnouncementsPatch {
// Do not show the announcement, if the last announcement id is the same as the current one. // Do not show the announcement, if the last announcement id is the same as the current one.
if (Settings.ANNOUNCEMENT_LAST_ID.get() == id) return; if (Settings.ANNOUNCEMENT_LAST_ID.get() == id) return;
int finalId = id; int finalId = id;
final var finalTitle = title; final var finalTitle = title;
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
@ -112,7 +111,7 @@ public final class AnnouncementsPatch {
Utils.runOnMainThread(() -> { Utils.runOnMainThread(() -> {
// Show the announcement. // Show the announcement.
var alertDialog = new android.app.AlertDialog.Builder(context) var alert = new AlertDialog.Builder(context)
.setTitle(finalTitle) .setTitle(finalTitle)
.setMessage(finalMessage) .setMessage(finalMessage)
.setIcon(finalLevel.icon) .setIcon(finalLevel.icon)
@ -123,11 +122,13 @@ public final class AnnouncementsPatch {
dialog.dismiss(); dialog.dismiss();
}) })
.setCancelable(false) .setCancelable(false)
.show(); .create();
// Make links clickable. Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
((TextView)alertDialog.findViewById(android.R.id.message)) // Make links clickable.
.setMovementMethod(LinkMovementMethod.getInstance()); ((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
}); });
} catch (Exception e) { } catch (Exception e) {
final var message = "Failed to get announcement"; final var message = "Failed to get announcement";

View File

@ -1,18 +1,5 @@
package app.revanced.integrations.youtube.settings; package app.revanced.integrations.youtube.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.integrations.shared.settings.Setting.*;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType;
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.*; import app.revanced.integrations.shared.settings.*;
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
@ -24,6 +11,19 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import static app.revanced.integrations.shared.settings.Setting.*;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch.ClientType;
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class Settings extends BaseSettings { public class Settings extends BaseSettings {
// Video // Video
@ -264,6 +264,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1);
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false); public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE); public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
// Debugging // Debugging
/** /**