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 a0275fb3..cdd474a9 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 @@ public class GmsCoreSupport {
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 25885050..b3729ef7 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 @@ public class Logger {
* 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 21a97a9a..22ed1e06 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.pm.PackageManager;
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 class Utils {
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 00000000..a9497d5b
--- /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("");
+ for (var check : failedChecks) {
+ // Add a non breaking space to fix bullet points spacing issue.
+ reasons.append("- ").append(check.failureReason());
+ }
+ 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 00000000..a782c7b2
--- /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 00000000..6ebf4d8f
--- /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 48c8fd8c..da294d72 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 class CheckWatchHistoryDomainNameResolutionPatch {
}
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 class CheckWatchHistoryDomainNameResolutionPatch {
}).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 eec599ec..225dc206 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 final class AnnouncementsPatch {
// 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 final class AnnouncementsPatch {
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 final class AnnouncementsPatch {
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 8708d579..5a40ed0f 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.SpoofAppVersionPatch;
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
/**