mirror of
https://github.com/revanced/revanced-integrations
synced 2024-11-19 13:57:14 +01:00
feat: Add Check environment
patch (#683)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
parent
a324b16096
commit
e856455283
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
* <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
|
||||
*/
|
||||
|
@ -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> ").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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = "";
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user