From e85645528336162e16acf89f7b9f029762972c72 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 6 Sep 2024 12:27:02 +0400 Subject: [PATCH] feat: Add `Check environment` patch (#683) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../integrations/shared/GmsCoreSupport.java | 21 +- .../revanced/integrations/shared/Logger.java | 31 +- .../revanced/integrations/shared/Utils.java | 81 ++++ .../integrations/shared/checks/Check.java | 164 ++++++++ .../shared/checks/CheckEnvironmentPatch.java | 369 ++++++++++++++++++ .../integrations/shared/checks/PatchInfo.java | 33 ++ ...WatchHistoryDomainNameResolutionPatch.java | 8 +- .../announcements/AnnouncementsPatch.java | 15 +- .../youtube/settings/Settings.java | 27 +- 9 files changed, 703 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/Check.java create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java create mode 100644 app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java diff --git a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java index a0275fb339..cdd474a994 100644 --- a/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java +++ b/app/src/main/java/app/revanced/integrations/shared/GmsCoreSupport.java @@ -54,18 +54,15 @@ private static void showBatteryOptimizationDialog(Activity context, String dialogMessageRef, String positiveButtonStringRef, DialogInterface.OnClickListener onPositiveClickListener) { - // Use a delay to allow the activity to finish initializing. - // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme. - Utils.runOnMainThreadDelayed(() -> { - new AlertDialog.Builder(context) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setTitle(str("gms_core_dialog_title")) - .setMessage(str(dialogMessageRef)) - .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) - // Allow using back button to skip the action, just in case the check can never be satisfied. - .setCancelable(true) - .show(); - }, 100); + // Do not set cancelable to false, to allow using back button to skip the action, + // just in case the check can never be satisfied. + var dialog = new AlertDialog.Builder(context) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("gms_core_dialog_title")) + .setMessage(str(dialogMessageRef)) + .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) + .create(); + Utils.showDialog(context, dialog); } /** diff --git a/app/src/main/java/app/revanced/integrations/shared/Logger.java b/app/src/main/java/app/revanced/integrations/shared/Logger.java index 2588505007..b3729ef7d9 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Logger.java +++ b/app/src/main/java/app/revanced/integrations/shared/Logger.java @@ -1,24 +1,21 @@ 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.shared.settings.BaseSettings; +import static app.revanced.integrations.shared.settings.BaseSettings.*; public class Logger { /** * Log messages using lambdas. */ + @FunctionalInterface public interface LogMessage { @NonNull String buildMessageString(); @@ -59,19 +56,33 @@ private String findOuterClassSimpleName() { * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. */ 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()) { - var messageString = message.buildMessageString(); + String logMessage = message.buildMessageString(); + String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); if (DEBUG_STACKTRACE.get()) { - var builder = new StringBuilder(messageString); + var builder = new StringBuilder(logMessage); var sw = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(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); + } } } 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 21a97a9a7f..22ed1e0624 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -1,6 +1,10 @@ package app.revanced.integrations.shared; 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.Intent; import android.content.pm.PackageInfo; @@ -8,6 +12,7 @@ import android.content.res.Resources; import android.net.ConnectivityManager; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.Preference; @@ -380,6 +385,82 @@ public static boolean containsNumber(@NonNull CharSequence text) { 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. + *
+ * 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. + *
+ * 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. + *
+ * 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 */ diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/Check.java b/app/src/main/java/app/revanced/integrations/shared/checks/Check.java new file mode 100644 index 0000000000..a9497d5b8c --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/Check.java @@ -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 failedChecks) { + final var reasons = new StringBuilder(); + + reasons.append(""); + + 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); + } + } + }; + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java new file mode 100644 index 0000000000..a782c7b297 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/CheckEnvironmentPatch.java @@ -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. + *
+ * 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. + *
+ * 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). + *
+ * 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. + *
+ * If the build properties are the same as during the patch, it is likely, the app was patched on the same device. + *
+ * 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. + *
+ * If the app was installed within the last 30 minutes, it is likely, the app was patched by the user. + *
+ * 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 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; + } + } +} diff --git a/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java b/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java new file mode 100644 index 0000000000..6ebf4d8fda --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/checks/PatchInfo.java @@ -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 = ""; + } +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java index 48c8fd8c35..da294d722e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -55,7 +55,7 @@ public static void checkDnsResolver(Activity context) { } 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")) .setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) .setIconAttribute(android.R.attr.alertDialogIcon) @@ -64,9 +64,9 @@ public static void checkDnsResolver(Activity context) { }).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); dialog.dismiss(); - }) - .setCancelable(false) - .show(); + }).create(); + + Utils.showDialog(context, alert, false, null); }); } catch (Exception ex) { Logger.printException(() -> "checkDnsResolver failure", ex); 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 eec599ecdf..225dc20609 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 @@ -1,6 +1,7 @@ package app.revanced.integrations.youtube.patches.announcements; import android.app.Activity; +import android.app.AlertDialog; import android.os.Build; import android.text.Html; import android.text.method.LinkMovementMethod; @@ -103,8 +104,6 @@ public static void showAnnouncement(final Activity context) { // 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; - - int finalId = id; final var finalTitle = title; final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); @@ -112,7 +111,7 @@ public static void showAnnouncement(final Activity context) { Utils.runOnMainThread(() -> { // Show the announcement. - var alertDialog = new android.app.AlertDialog.Builder(context) + var alert = new AlertDialog.Builder(context) .setTitle(finalTitle) .setMessage(finalMessage) .setIcon(finalLevel.icon) @@ -123,11 +122,13 @@ public static void showAnnouncement(final Activity context) { dialog.dismiss(); }) .setCancelable(false) - .show(); + .create(); - // Make links clickable. - ((TextView)alertDialog.findViewById(android.R.id.message)) - .setMovementMethod(LinkMovementMethod.getInstance()); + Utils.showDialog(context, alert, false, (AlertDialog dialog) -> { + // Make links clickable. + ((TextView) dialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + }); }); } catch (Exception e) { final var message = "Failed to get announcement"; diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 8708d579c5..5a40ed0fe8 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -1,18 +1,5 @@ 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.settings.*; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; @@ -24,6 +11,19 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofClientPatch; 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") public class Settings extends BaseSettings { // 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 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 IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); // Debugging /**